diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000..7347f66f20 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + stage: 0 +} \ No newline at end of file diff --git a/.bookignore b/.bookignore new file mode 100644 index 0000000000..bd268a7a0a --- /dev/null +++ b/.bookignore @@ -0,0 +1,26 @@ +build/ +config/ +dist/ +fonts/ +images/ +lib/**/*.js +lib/**/*.jsx +lib/carelink/ +lib/components/ +lib/core/ +lib/dexcom/ +lib/insulet/ +lib/redux/ +lib/state/ +lib/tandem/ +node_modules/ +scripts/ +styles/ +test/ +**/*.js +.eslintrc +.travis.yml +description.txt +LICENSE +manifest.json +package.json diff --git a/.config.js b/.config.js index 9c128e5e57..e0c86d6891 100644 --- a/.config.js +++ b/.config.js @@ -33,12 +33,13 @@ function stringToArray(str, defaultValue) { } module.exports = { + // this is to always have the Bows logger turned on! + // NB: it is distinct from our own "debug mode" DEBUG: stringToBoolean(process.env.DEBUG, true), // the defaults for these need to be pointing to prod API_URL: process.env.API_URL || 'https://api.tidepool.org', UPLOAD_URL: process.env.UPLOAD_URL || 'https://uploads.tidepool.org', BLIP_URL: process.env.BLIP_URL || 'https://blip.tidepool.org', - CARELINK: stringToBoolean(process.env.CARELINK, true), DEFAULT_TIMEZONE: process.env.DEFAULT_TIMEZONE || 'America/Los_Angeles', DEFAULT_CARELINK_DAYS: process.env.DEFAULT_CARELINK_DAYS || '180' }; diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..dce3bdd6c0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,54 @@ +{ + // I want to use babel-eslint for parsing! + "parser": "babel-eslint", + "ecmaFeatures": { + "jsx": true, + "classes": true, + "modules": true, + }, + "env": { + // I write for browser + "browser": true, + // in CommonJS + "node": true + }, + "globals": { + "expect": false, + "process": false, + "require": false, + "define": false, + "console": false, + "__MOCK__": false, + "__MOCK_PARAMS__": false, + "__UPLOAD_API__": false, + "__API_HOST__": false, + "__SHOW_ACCEPT_TERMS__": false, + "__PASSWORD_MIN_LENGTH__": false, + "__INVITE_KEY__": false + }, + // To give you an idea how to override rule options: + "rules": { + "quotes": [2, "single"], + "strict": [2, "never"], + "eol-last": 0, + "no-mixed-requires": 0, + "no-underscore-dangle": 0, + "wrap-iife": [2, "inside"], + "no-caller": 2, + "no-undef": 2, + "new-cap": 2, + "semi": 2, + "react/jsx-uses-react": 2, + "react/jsx-uses-vars": 2, + "react/react-in-jsx-scope": 2, + // uploader specific + "camelcase": 0, + "eqeqeq": 0, + "no-bitwise": 0, + // TODO: either try to fix this globally or use an embedded .eslintrc for drivers + "no-use-before-define": 0 + }, + "plugins": [ + "react" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index fbc6fa345d..bea7b1915d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,14 +11,17 @@ pids logs results config/* -!config/mock.sh +!config/device-debug.sh !config/local.sh +!config/ui-debug.sh build dist dist.zip test/carelink/overlaps/test-account.csv +test/node/carelink/overlaps/test-account.csv test/carelink/testRemoveOverlapsFullTestFile.js +test/node/carelink/testRemoveOverlapsFullTestFile.js tmp npm-debug.log @@ -28,4 +31,7 @@ bower_components .DS_Store .idea *.iml -.com.apple.timemachine.* \ No newline at end of file +.com.apple.timemachine.* + +_book/ +web/ diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index aa6f3031e9..0000000000 --- a/.jshintrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - // Details: https://github.com/victorporof/Sublime-JSHint#using-your-own-jshintrc-options - // Example: https://github.com/jshint/jshint/blob/master/examples/.jshintrc - // Documentation: http://www.jshint.com/docs/ - "browser": true, - "curly": true, - "esnext": true, - "eqnull": true, - "globalstrict": true, - "indent": 2, - "laxcomma": true, - "loopfunc": true, - "node": true, - "quotmark": true, - "smarttabs": false, - "strict": false, - "sub": true, - "trailing": true, - "undef": true -} diff --git a/.travis.yml b/.travis.yml index 4c353b3287..9c41053338 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ node_js: - "0.12" - "stable" script: -- npm run jshint +- npm run lint - npm test matrix: diff --git a/README.md b/README.md index 7cc33fb0d2..3b886020c9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/tidepool-org/chrome-uploader.png)](https://travis-ci.org/tidepool-org/chrome-uploader) -This is a Chrome App that acts as an uploader client for Tidepool. It is intended to allow you to plug devices into the USB port and automatically load the data stored on them up to the Tidepool cloud. +This is a [Chrome App](https://developer.chrome.com/apps/about_apps) that acts as an uploader client for Tidepool. It is intended to allow you to plug diabetes devices into the USB port and automatically load the data stored on them up to the Tidepool cloud. ## How to set it up @@ -15,6 +15,7 @@ This is a Chrome App that acts as an uploader client for Tidepool. It is intende 1. Click "Load Unpacked Extension". 1. Choose the directory where you cloned the repository and click OK. 1. To run it, you can choose "Launch" from the chrome://extensions page. You can also run it from the Chrome App Launcher, which Chrome may install for you whether you want it or not. +1. To open the JavaScript console/Chrome Dev Tools, click on the `index.html` link in the section of chrome://extensions devoted to the uploader. (Note: this link will only appear after you've launched the uploader.) 1. If you're developing, you may find that the only way it runs properly is to hit the "Reload" link in chrome://extensions after each change to the source. You will definitely need to reload any time you change the manifest. @@ -27,13 +28,75 @@ $ source config/local.sh $ npm start ``` -### Debug Mode +### Debug Mode(s) -The environment variable `DEBUG_ERROR` (boolean) controls whether or not errors are caught and an error message displayed in the UI (the production setting) or whether they are thrown in the console (much more useful for local development because then the file name and line number of the error are easily accessible, along with a stack trace). Debug mode is turned on by default in `config/debug.sh`. +For ease of development we have several debug features that developers can turn on and off at will (and to suit various development use cases, such as working on a new device driver versus working on the app's UI). Each of these debug features is set with an environment variable, but rather than being loaded through `.config.js` (as we do for production configuration variables, see above), we load these through the webpack `DefinePlugin` (see [Pete Hunt's webpack-howto](https://github.com/petehunt/webpack-howto#6-feature-flags) for an example, although note Hunt uses the term 'feature flag'). -## How to run the tests +#### `DEBUG_ERROR` -```npm test``` +The environment variable `DEBUG_ERROR` (boolean) controls whether or not errors sourced in device drivers are caught and an error message displayed in the UI (the production setting) or whether they are thrown in the console (much more useful for local development because then the file name and line number of the error are easily accessible, along with a stack trace). `DEBUG_ERROR` mode is turned on by default in `config/device-debug.sh`. + +#### `REDUX_LOG` + +The environment variable `REDUX_LOG` (boolean) controls whether or not the [redux logger middleware](https://github.com/fcomb/redux-logger/blob/master/README.md) is included. This middleware logs all redux actions in the Chrome developer console, including the (entire) previous and following app state trees. It is primarily useful when working on the UI of the app, and in fact can be quite performance-expensive (especially when uploading a device, due to the fact that every update to the progress bar constitutes an action), so it is not recommended to turn it on while working on device code. + +#### `REDUX_DEV_UI` + +The environment variable `REDUX_DEV_UI` (boolean) controls whether or not the [redux dev tools UI](https://github.com/gaearon/redux-devtools/blob/master/README.md) is included. The redux dev tools add a UI interface for exploring - and, to a limited extent, manipulating - app actions and state. Even when `REDUX_DEV_UI` is `true`, we have the dev tools hidden by default: the key combination `ctrl + h` will toggle their visibility. The key combination `ctrl + q` will rotate (clockwise) the location at which the dev tools are anchored; the default is for them to be anchored at the bottom of the app. Similarly to the redux logger middleware, the redux dev tools UI is also quite performance expensive and only recommended for use while working on UI code. + +`REDUX_LOG` and `REDUX_DEV_UI` are both turned on by default in `config/ui-debug.sh`. + +### Local Development w/o Debug Mode(s) + +All debug options are turned *off* by default in `config/local.sh`. + + +## Tests + +There are two sets of (unit) tests for the code in this repository. + +The tests for all device and data-processing code currently run in the [nodejs](https://nodejs.org/en/) server-side JavaScript environment. (We plan to eventually migrate these tests to run in-browser since the code itself runs in-browser in the Chrome App.) + +The tests for all the UI code run using the [Karma test runner](https://karma-runner.github.io/0.13/index.html) in [the headless WebKit browser PhantomJS](http://phantomjs.org/) or the Chrome browser. + +To run the tests in this repository as they are run on Travis CI, use: + +```bash +$ npm test +``` + +To run just the UI tests in both PhantomJS and Chrome *locally*, use: + +```bash +$ npm run browser-tests +``` + +To run just the device and data-processing tests in node, use: + +```bash +$ npm run node-tests +``` + +To run just the UI tests in PhantomJS with webpack & Karma watching all files for changes and both rebundling the app and re-running the tests on every change, use: + +```bash +$ npm run karma-watch +``` + + +## Linting & Code Style + +We use [ESLint](http://eslint.org/) to lint our JavaScript code. We try to use the same linting options across all our client apps, but there are a few exceptions in this application, noted with comments in the `.eslintrc` configuration file. + +To run the linter (which also runs on Travis CI with every push, along with `npm test`), use: + +``` +$ npm run lint +``` + +Aside from the (fairly minimal) JavaScript code style options we *enforce* through the linter, we ask that internal developers and external contributors try to match the style of the code in each module being modified. New modules should look to similar modules for style guidance. In React component code, use existing ES6/ES2015 components (not legacy ES5 components) as the style model for new components. + +**NB: Please keep ES5 and ES6/ES2015 code distinct. Do *NOT* use ES6/ES2105 features in ES5 modules (most easily recognizable by the use of `require` rather than `import`).** ## Publishing (to the devel/staging testing & development Chrome store account or production) @@ -48,4 +111,4 @@ Assuming you've already merged any changes to master and are on master locally.. 1. Make sure you are using node v0.12.0 and install fresh dependencies with `npm install`. 1. Build the `dist.zip` file with `npm run build`. Look for the "**Using the default environment, which is now production**" message at the beginning of the build process. (You can check the success of a build (prior to publishing) by pointing 'Load unpacked extension' from chrome://extensions to the `dist/` subdir.) 1. Follow instructions in secrets for actually publishing to the Chrome store. -1. Fill out the release notes for the tag on GitHub. If the tag is known to *not* be a release candidate, mark as a pre-release. +1. Fill out the release notes for the tag on GitHub. If the tag is known to *not* be a release candidate, mark it as a pre-release. diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000000..dd0d4cd221 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,29 @@ +# Summary + +* [application state](docs/state/README.md) + * [Example state tree](docs/state/ExampleStateTree.md) + * [Glossary of state tree terms](docs/state/StateTreeGlossary.md) + * [Custom middleware](docs/state/CustomMiddleware.md) + +* ["bootstrapping" to UTC (BtUTC)](docs/BootstrappingToUTC.md) + +* [checklist TEMPLATES](docs/checklisttemplates/README.md) + * [BGM Checklist](docs/checklisttemplates/BGMChecklist.md) + * [CGM Checklist](docs/checklisttemplates/CGMChecklist.md) + * [Insulin Pump Checklist](docs/checklisttemplates/PumpChecklist.md) + +* [checklists](lib/drivers/docs/README.md) + * [Abbott FreeStyle](lib/drivers/docs/abbottFreeStyleLite.md) + * [Abbott Precision Xtra](lib/drivers/docs/abbottPrecisionXtra.md) + * [Animas Ping and Vibe Insulin Pumps](lib/drivers/docs/animasPingAndVibe.md) + * [Bayer Contour Next](lib/drivers/docs/bayerContourNext.md) + * [Dexcom CGM](lib/drivers/docs/dexcom.md) + * [CareLink (CGM data)](lib/drivers/docs/carelinkCGM.md) + * [CareLink (insulin pump data)](lib/drivers/docs/carelinkPumpData.md) + * [Insulet OmniPod](lib/drivers/docs/insuletOmniPod.md) + * [Tandem Insulin Pumps](lib/drivers/docs/tandem.md) + +* [miscellaneous](docs/misc/README.md) + * [PWD "simulators"](docs/misc/PWDSimulators.md) + * [challenges of flat basal profiles](docs/misc/FlatRateBasals.md) + * [2015.06 CareLink CSV updates](docs/misc/2015.06.29CareLinkCSVChanges.md) diff --git a/browser.tests.js b/browser.tests.js new file mode 100644 index 0000000000..74eeeed13f --- /dev/null +++ b/browser.tests.js @@ -0,0 +1,2 @@ +var context = require.context('./test/browser', true, /\.js$|\.jsx$/); // Load files in /test/browser/ with filename matching * .js +context.keys().forEach(context); diff --git a/config/debug.sh b/config/device-debug.sh similarity index 74% rename from config/debug.sh rename to config/device-debug.sh index 07b14f5217..e7578a122b 100644 --- a/config/debug.sh +++ b/config/device-debug.sh @@ -2,3 +2,5 @@ export API_URL='http://localhost:8009' export UPLOAD_URL='http://localhost:9122' export BLIP_URL='http://localhost:3000' export DEBUG_ERROR=true +export REDUX_LOG=false +export REDUX_DEV_UI=false diff --git a/config/local.sh b/config/local.sh index a55ce0ecc6..719ee0ad65 100644 --- a/config/local.sh +++ b/config/local.sh @@ -2,3 +2,5 @@ export API_URL='http://localhost:8009' export UPLOAD_URL='http://localhost:9122' export BLIP_URL='http://localhost:3000' export DEBUG_ERROR=false +export REDUX_LOG=false +export REDUX_DEV_UI=false diff --git a/config/ui-debug.sh b/config/ui-debug.sh new file mode 100644 index 0000000000..2a3e4590fe --- /dev/null +++ b/config/ui-debug.sh @@ -0,0 +1,6 @@ +export API_URL='http://localhost:8009' +export UPLOAD_URL='http://localhost:9122' +export BLIP_URL='http://localhost:3000' +export DEBUG_ERROR=false +export REDUX_LOG=true +export REDUX_DEV_UI=true diff --git a/docs/BGMChecklist.md b/docs/checklisttemplates/BGMChecklist.md similarity index 100% rename from docs/BGMChecklist.md rename to docs/checklisttemplates/BGMChecklist.md diff --git a/docs/CGMChecklist.md b/docs/checklisttemplates/CGMChecklist.md similarity index 100% rename from docs/CGMChecklist.md rename to docs/checklisttemplates/CGMChecklist.md diff --git a/docs/PumpChecklist.md b/docs/checklisttemplates/PumpChecklist.md similarity index 100% rename from docs/PumpChecklist.md rename to docs/checklisttemplates/PumpChecklist.md diff --git a/docs/checklisttemplates/README.md b/docs/checklisttemplates/README.md new file mode 100644 index 0000000000..51e6c32cae --- /dev/null +++ b/docs/checklisttemplates/README.md @@ -0,0 +1,5 @@ +Templates of checklists for implementations of drivers to read data from diabetes devices. + +- [blood glucose meters (BGMs)](BGMChecklist.md) +- [continuous glucose monitors (CGMs)](CGMChecklist.md) +- [insulin pumps](PumpChecklist.md) \ No newline at end of file diff --git a/docs/2015.06.29 CareLink CSV Changes.md b/docs/misc/2015.06.29CareLinkCSVChanges.md similarity index 100% rename from docs/2015.06.29 CareLink CSV Changes.md rename to docs/misc/2015.06.29CareLinkCSVChanges.md diff --git a/docs/FlatRateBasals.md b/docs/misc/FlatRateBasals.md similarity index 100% rename from docs/FlatRateBasals.md rename to docs/misc/FlatRateBasals.md diff --git a/docs/PWDSimulators.md b/docs/misc/PWDSimulators.md similarity index 100% rename from docs/PWDSimulators.md rename to docs/misc/PWDSimulators.md diff --git a/docs/misc/README.md b/docs/misc/README.md new file mode 100644 index 0000000000..905033a4a8 --- /dev/null +++ b/docs/misc/README.md @@ -0,0 +1,5 @@ +Miscellaneous technical documents. + +- guidance documentation for ["simulator" modules](PWDSimulators.md) accompanying our insulin pump drivers +- technical explanation of the challenges of ["flat" basal profiles (on insulin pumps)](FlatRateBasals.md) +- documentation of [updates to the CareLink CSV export format made late June 2015](2015.06.29CareLinkCSVChanges.md) \ No newline at end of file diff --git a/docs/state/CustomMiddleware.md b/docs/state/CustomMiddleware.md new file mode 100644 index 0000000000..61c2b7a6e4 --- /dev/null +++ b/docs/state/CustomMiddleware.md @@ -0,0 +1,11 @@ +## Custom Middleware + +One of the great benefits of [redux](http://redux.js.org/) is the easy path it provides for writing middleware to perform various actions in response to some or all of the redux actions that are the source of all changes to the application's state tree. The open-source community provides some great middleware options like the [redux logger](https://github.com/fcomb/redux-logger) that we include behind an environment variable to assist in development. + +In the Tidepool Uploader, we also include two custom middlewares: one for making calls to our metrics API and one for logging application errors. + +The source for the metrics middleware is found in `lib/redux/utils/metrics.js`. It performs a call to the Tidepool metrics API for any redux action that includes a `metric` property inside its `meta` property. + +The source of the error-logging middleware is found in `lib/redux/utils/errors.js`. It performs a call to the Tidepool server-side error logging for any redux action that has the boolean flag `error` as true and a JavaScript `Error` object as its `payload`. + +If the source code of our custom middlewares confuses more than it answers questions, we recommended reading the excellent [intro to middleware](http://redux.js.org/docs/advanced/Middleware.html) included in the redux documentation. diff --git a/docs/state/ExampleStateTree.md b/docs/state/ExampleStateTree.md new file mode 100644 index 0000000000..ed846b2109 --- /dev/null +++ b/docs/state/ExampleStateTree.md @@ -0,0 +1,243 @@ +## An Example State Tree + +![Tidepool Uploader snapshot](./app-snapshot.png) + +The JSON that follows on this page represents a snapshot of the Tidepool Uploader's application state as shown in the above screenshot. We provide this example mainly as a reference to use while reading the [glossary of terms](./StateTreeGlossary.md) for the Tidepool Uploader's redux-managed state tree. + + +```json +{ + "devices": { + "carelink": { + "instructions": ["Import from CareLink", "(We will not store your credentials)"], + "isFetching": false, + "key": "carelink", + "name": "Medtronic", + "selectName": "Medtronic (from CareLink)", + "showDriverLink": { + "mac": false, + "win": false + }, + "source": { + "type": "carelink" + }, + "enabled": { + "mac": true, + "win": true + } + }, + "omnipod": { + "instructions": "Choose .ibf file from PDM", + "key": "omnipod", + "name": "Insulet OmniPod", + "showDriverLink": { + "mac": false, + "win": false + }, + "source": { + "type": "block", + "driverId": "InsuletOmniPod", + "extension": ".ibf" + }, + "enabled": { + "mac": true, + "win": true + } + }, + "dexcom": { + "instructions": "Plug in receiver with micro-USB", + "key": "dexcom", + "name": "Dexcom", + "showDriverLink": { + "mac": true, + "win": true + }, + "source": { + "type": "device", + "driverId": "Dexcom" + }, + "enabled": { + "mac": true, + "win": true + } + }, + "tandem": { + "instructions": "Plug in pump with micro-USB", + "key": "tandem", + "name": "Tandem", + "showDriverLink": { + "mac": false, + "win": true + }, + "source": { + "type": "device", + "driverId": "Tandem" + }, + "enabled": { + "mac": true, + "win": true + } + }, + "bayercontournext": { + "instructions": "Plug in meter with micro-USB", + "key": "bayercontournext", + "name": "Bayer Contour Next", + "showDriverLink": { + "mac": false, + "win": false + }, + "source": { + "type": "device", + "driverId": "BayerContourNext" + }, + "enabled": { + "mac": true, + "win": true + } + }, + "bayercontournextusb": { + "instructions": "Plug meter into USB port", + "key": "bayercontournextusb", + "name": "Bayer Contour Next USB", + "showDriverLink": { + "mac": false, + "win": false + }, + "source": { + "type": "device", + "driverId": "BayerContourNextUsb" + }, + "enabled": { + "mac": true, + "win": true + } + }, + "bayercontourusb": { + "instructions": "Plug meter into USB port", + "key": "bayercontourusb", + "name": "Bayer Contour USB", + "showDriverLink": { + "mac": false, + "win": false + }, + "source": { + "type": "device", + "driverId": "BayerContourUsb" + }, + "enabled": { + "mac": true, + "win": true + } + }, + "bayercontournextlink": { + "instructions": "Plug meter into USB port", + "key": "bayercontournextlink", + "name": "Bayer Contour Next Link", + "showDriverLink": { + "mac": false, + "win": false + }, + "source": { + "type": "device", + "driverId": "BayerContourNextLink" + }, + "enabled": { + "mac": true, + "win": true + } + } + }, + "dropdown": true, + "page": "MAIN", + "unsupported": false, + "blipUrls": { + "forgotPassword": "http://localhost:3000/request-password-from-uploader", + "signUp": "http://localhost:3000/signup", + "viewDataLink": "http://localhost:3000/patients/4a86ec44ff/data" + }, + "working": { + "checkingVersion": false, + "fetchingUserInfo": false, + "initializingApp": false, + "uploading": false + }, + "uploadProgress": null, + "uploadsByUser": { + "4a86ec44ff": { + "carelink": { + "history": [] + }, + "bayercontournextlink": { + "history": [] + } + }, + "77541c89ba": { + "omnipod": { + "history": [] + }, + "dexcom": { + "history": [] + } + }, + "a6328f570d": { + "tandem": { + "history": [] + } + }, + "a9c0de41c5": { + "carelink": { + "history": [] + } + } + }, + "uploadTargetDevice": null, + "allUsers": { + "77541c89ba": { + "fullName": "John Doe", + "patient": { + "birthday": "2014-07-04", + "diagnosisDate": "2016-01-01", + "about": "I'm just a baby!", + "isOtherPerson": true, + "fullName": "Baby Doe" + } + }, + "a6328f570d": { + "fullName": "Mary Doe", + "patient": { + "birthday": "1982-03-15", + "diagnosisDate": "2000-12-25", + "about": "I'm Jane's sister, and I also have type 1." + } + }, + "4a86ec44ff": { + "fullName": "Jane Doe", + "patient": { + "birthday": "1980-07-04", + "diagnosisDate": "1999-04-01", + "about": "No thanks :P" + } + }, + "4fdc9dd8b4": { + "username": "drdoe+skip@tidepool.org", + "emails": ["drdoe+skip@tidepool.org"], + "termsAccepted": "2016-02-01T15:04:42-08:00", + "emailVerified": false, + "fullName": "Doctor Doe" + } + }, + "loggedInUser": "4fdc9dd8b4", + "loginErrorMessage": null, + "targetDevices": { + "77541c89ba": ["omnipod", "dexcom"], + "a6328f570d": ["tandem"], + "4a86ec44ff": ["carelink", "bayercontournextlink"] + }, + "targetTimezones": { + "77541c89ba": "US/Mountain", + "4a86ec44ff": "US/Central" + }, + "targetUsersForUpload": ["77541c89ba", "a6328f570d", "4a86ec44ff"], + "uploadTargetUser": "4a86ec44ff" +} +``` diff --git a/docs/state/README.md b/docs/state/README.md new file mode 100644 index 0000000000..56c991164f --- /dev/null +++ b/docs/state/README.md @@ -0,0 +1,9 @@ +Reference documentation for application/UI state in the Tidepool Uploader. + +As of February of 2016, we have migrated the application state (including UI state) management in the Tidepool Uploader to [redux](http://redux.js.org/). Redux is a lightweight but powerful state container for JavaScript applications that takes inspiration equally from (a) Facebook's [Flux](https://facebook.github.io/flux/) application architecture (especially its emphasis on one-way data flow) and (b) functional programming, in particular [Elm](http://elm-lang.org/), a functional programming language for building GUIs on the web. + +The redux documentation is very well-written. If you are unfamiliar with the library, we recommended starting with the redux [Basics](http://redux.js.org/docs/basics/index.html) docs to familiarize yourself with the standard redux vocabulary before reading more of the documentation here about our use of redux in the Tidepool Uploader. + +- [Example state tree](ExampleStateTree.md) +- [Glossary of state tree terms](StateTreeGlossary.md) +- [Custom middleware](CustomMiddleware.md) diff --git a/docs/state/StateTreeGlossary.md b/docs/state/StateTreeGlossary.md new file mode 100644 index 0000000000..d1ea528c80 --- /dev/null +++ b/docs/state/StateTreeGlossary.md @@ -0,0 +1,125 @@ +## Glossary of Terms in the Tidepool Uploader's State Tree + +### Preliminary + +> **PWD**: Person With Diabetes (or, conveniently enough for us at Tidepool, Person With Data). Used as a shorthand for a user that has a Tidepool account *with* data storage, as opposed to a Tidepool user (such as a clinic worker, diabetes educator, endocrinologist etc.) whose account is not set up for data storage. + +### User-Related State + +All the state that is driven by user information and/or the login process is handled in the reducers in `lib/redux/reducers/users.js`. + +#### `allUsers` + +*The `allUsers` property is an object keyed by the user IDs of the logged-in user as well as all the PWDs the logged-in user has permissions to upload for. The information stored under each `userID` is all the account and profile information accessible to the logged-in user for that user.* + +In essence, the `allUsers` object is where all user information accessible to the logged-in user is stored. Keeping all this information in one place keyed by `userId` allows us to only reference users by `userId` elsewhere in the state tree, thus maintaining a "normalized" state tree with a single source of truth for user-related information. (Cf. Dan Abramov's presentation of the "normalization" problem in the README for his [normalizr library](https://github.com/gaearon/normalizr#the-problem).) Whenever a component needs additional information (that is, beyond `userId`) about a user, that information can be retrieved via lookup under the `userId`. + +Examples of properties that may be encoded in `allUsers` for a particular user include `fullName`, `emails`, a `patient` object that itself includes the PWD's `birthday` and `diagnosisData`. Also see the [example state tree](./ExampleStateTree.md) for full examples. + +#### `loggedInUser` + +*The property `loggedInUser` encodes the `userID` of the currently logged-in user.* + +If no user is (yet) logged in, the value of `loggedInUser` is `null`. + +#### `loginErrorMessage` + +*The property `loginErrorMessage` encodes the error message if there is an error on an attempt to log in.* + +If there has not (yet) been a login attempt or login proceeded without error, the value of `loginErrorMessage` is `null`. + +#### `targetDevices` + +*The property `targetDevices` is an object keyed by the user IDs of all the PWDs the logged-in user has permissions to upload for (including the logged-in user, if applicable). The information stored under each `userId` is an array of the `deviceKey`s selected for that user as potential sources of data to upload to the Tidepool cloud.* + +#### `targetTimezones` + +*The property `targetTimezones` is an object keyed by the user IDs of all the PWDs the logged-in user has permissions to upload for (including the logged-in user, if applicable). The information stored under each `userId` is a string timezone name (from the [IANA Time Zone Database](http://www.iana.org/time-zones) by way of the [Moment Timezone](http://momentjs.com/timezone/) JavaScript library).* + +In the future we plan to support the possibility of selecting a timezone for each device a user has selected as a source of data to upload to Tidepool. When we introduce such support, we will change the value of the information stored under each `userId` in `targetTimezones` to an object keyed by the `deviceKey`s selected by the user. + +#### `targetUsersForUpload` + +*The property `targetUsersForUpload` is an array consisting of the `userId`s of all the PWDs the logged-in user has permissions to upload for (including the logged-in user, if applicable).* + +This array drives the user selection dropdown menu that provides the interface for setting and changing the `uploadTargetUser`. + +#### `uploadTargetUser` + +*The propery `uploadTargetUser` encodes the `userId` (for lookup in the [`allUsers`](#-allusers) branch of the state tree) of the PWD currently selected as the target for data upload.* + +The combination of `uploadTargetUser` and `uploadTargetDevice` provides the path into `uploadsByUser` to the upload currently in progress, if any. + +* * * + +### (Current) Upload-Related State + +All the state that is driven by the current (i.e., in progress) or recent upload(s) is handled in the reducers in `lib/redux/reducers/uploads.js`. + +#### `uploadProgress` + +*The `uploadProgress` property is an object with two keys: `percentage` to record the percentage towards completion of the current upload in progress and `step` to encode the current step in the upload sequence (e.g., device detection, device read, POSTing data to the Tidepool data ingestion API).* + +When there is not an upload in progress, the value of `uploadProgress` is `null`. + +#### `uploadsByUser` + +*The `uploadsByUser` property is an object keyed by the user IDs of all the PWDs the logged-in user has permissions to upload for (including the logged-in user, if applicable). Within each user ID is another object keyed by the devices that user has selected to upload data from, if any. The information stored at each userId, deviceKey path is semi-ephemeral information about the state of current and recent uploads.* + +One example of a property that is encoded in the object at the termination of each `userId`, `deviceKey` path is the `history` of the user's uploading for that device, which is an array of objects with up to a maximum of `start` and `finish` timestamps and a boolean `error` to encode whether the upload was successful. Other examples are additional boolean flags regarding the upload's state: `completed`, `failed`, `successful`, and `uploading`. If an upload failed due to an error, the error object itself is included in an `error` field. For block-mode devices, there are additional flags and fields such as boolean flags for `choosingFile` and `readingFile` and `file` object encoding the `name` and `data` from a selected file. + +#### `uploadTargetDevice` + +*The property `uploadTargetDevice` encodes the `key` (for lookup in the [`devices`](#-devices) branch of the state tree) of the device currently being uploaded when an upload is in progress.* + +When an upload is *not* in progress, the value of `uploadTargetDevice` is `null`. The combination of `uploadTargetUser` and `uploadTargetDevice` provides the path into `uploadsByUser` to the upload currently in progress, if any. + +* * * + +### All Other App State + +All other app state is handled in the reducers in `lib/redux/reducers/misc.js`. + +#### `blipUrls` + +*The property `blipUrls` is an object with three properties: `forgotPassword`, `signUp`, and `viewDataLink`, all URLs for Tidepool's webapp blip.* + +The `forgotPassword` and `signUp` links are built as part of the app initialization step for the configured or chosen server environment (the default is production). + +The `viewDataLink` is built every time the `uploadTargetUser` is chosen or changed. + +**NB:** Storing these URLs as state is not ideal. Both the forgot password and sign-up URLs are essentially *derived* state - derived from a combination of route paths (e.g., `/signup`), which are constants, and a single piece of state - the server environment. The `viewDataLink` is also derived state from a combination of route paths, the server environment, and the `uploadTargetUser`. For now, we are keeping these URLs in the state tree because we do *not* represent the server environment in the state tree. We don't represent the server environment in the state tree because we have code running separately from the main application (in the Chrome App's "background page") that provides a hidden interface (for internal Tidepool use) for changing the server environment. + +#### `devices` + +*The `devices` property on the state tree is an object keyed by the `id` of each device (or data source) supported by the Tidepool Uploader.* + + `devices` *almost* does not belong in the state tree at all, because it is *almost* a constant. However, it is subject to filtering based on operating system; this filtering happens as part of the app initialization step when a user launches the Tidepool Uploader. The property `enabled` - itself an object with `mac` and `win` as its keys - encodes the devices Tidepool currently support on each platform. Similarly, the property `showDriverLink` encodes, for each device, whether the [Tidepool USB driver](http://tidepool.org/downloads/) is required in order to upload the device on each platform. + +The properties of each "device" in `devices` should be fairly self-explanatory. For example, the `instructions` property stores the text that appears in the UI under each device name to give the user some indication of how to proceed (e.g., what type of cable is required to connect a particular device). + +#### `dropdown` + +*The Tidepool Uploader inclues a dropdown menu, which is accessible after logging in by clicking on the area where the logged-in user's name is displayed in the upper-right corner. The property `dropdown` in the state tree encodes whether this menu is currently in its open (dropped-down) state (`true`) or closed and hidden (`false`).* + +#### `page` + +*The property `page` encodes the current "page" of the application that is active. Possible values are, for example, `SETTINGS` for the device selection "page" and `MAIN` for the main upload interface shown in [the app snapshot](./ExampleStateTree.md).* + +Since the Tidepool Uploader is a Chrome App that runs *outside* of the browser environment and thus does not have URL-based navigation, we use the simple `page` variable and nothing more to record the state that in a browser-based single-page app might be managed by a larger, more robust routing solution such as [React Router](https://github.com/rackt/react-router). As the functionality in the uploader grows - and especially if the addition of *back* and *forward* navigation buttons is planned - we may revisit this decision and bring in a more featureful single-page app routing solution. + +#### `unsupported` + +*The propery `unsupported` encodes whether the running version of the Tidepool Uploader is outdated from the perspective of Tidepool's data ingestion API. **This property defaults to true**; in other words, any instance of the uploader is assumed to be outdated and unsupported until it proves itself otherwise.* + +To ensure the highest possible standards of data quality, it is very important for us at Tidepool to prevent uploaders that have been succeeded by newer versions from uploading to the Tidepool cloud. To this end, we have implemented an "info" endpoint on our data ingestion API that responds with (among other things) the minimum version of the Tidepool Uploader that the data ingestion API will accept data from. + +#### `working` + +*The `working` property is an object with a small handful of keys that record the app's current state with respect to certain asynchronous actions.* + +The properties `initializingApp` (which defaults to `true`) and `checkingVersion` serve to prevent rendering the warning message about the Tidepool Uploader being unsupported before the application has finished checking against the Tidepool data ingestion API to determine whether it is outdated and unsupported. (See [unsupported](#-unsupported) above, taking care to note that `unsupported` defaults to `true`, so without some other indicator(s) of the app's state with respect to validation of the current version against the Tidepool data ingestion API, the "uploader unsupported" warning message would render immediately.) + +The property `fetchingUserInfo` is used to render a "Logging in..." message after a user's credentials have been submitted but the uploader is still waiting for a (complete) response from the Tidepool platform with the logged-in user's information. + +Finally, the property `uploading` is used to disable certain UI features while an upload is in progress. When `uploading` is true, the dropdown menu for selecting the `uploadTargetUser` as well as the link to "Choose devices" in the dropdown menu are disabled until the current upload is completed, as changing the target user for upload and/or the devices chosen for upload while an upload is in progress for a particular user and device is not supported behavior. diff --git a/docs/state/app-snapshot.png b/docs/state/app-snapshot.png new file mode 100644 index 0000000000..9b35582e35 Binary files /dev/null and b/docs/state/app-snapshot.png differ diff --git a/entry.js b/entry.js index 4add50041d..6162f438cc 100644 --- a/entry.js +++ b/entry.js @@ -15,15 +15,23 @@ * == BSD2 LICENSE == */ +/* global chrome */ + require('./styles/main.less'); +var _ = require('lodash'); var React = require('react'); +var ReactDOM = require('react-dom'); window.React = React; var config = require('./lib/config'); window.DEBUG = config.DEBUG; // Important: need to require App after setting `window.DEBUG` to enable logging -var App = require('./lib/components/App.jsx'); +var Root = require('./lib/containers/root/Root'); -window.app = React.render( - React.createElement(App), document.body -); +chrome.runtime.getPlatformInfo(function (platformInfo) { + if (!_.isEmpty(platformInfo.os)) { + ReactDOM.render( + React.createElement(Root, {os: platformInfo.os}), document.getElementById('app') + ); + } +}); diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index c40a70ebfa..0000000000 --- a/gulpfile.js +++ /dev/null @@ -1,37 +0,0 @@ -var gulp = require('gulp'); -var jshint = require('gulp-jshint'); -var react = require('gulp-react'); -var merge = require('merge-stream'); - -var jsFiles = [ - 'lib/**/*.js', - 'test/**/*.js', - '*.js' -]; - -var jsxFiles = [ - 'lib/**/*.jsx' -]; - -gulp.task('jshint', function() { - var js = gulp.src(jsFiles); - var jsx = gulp.src(jsxFiles) - .pipe(react()); - - var stream = merge(js, jsx) - .pipe(jshint()) - .pipe(jshint.reporter('jshint-stylish')); - - if (process.env.CI) { - stream = stream.pipe(jshint.reporter('fail')); - } - - return stream; -}); - -gulp.task('jshint-watch', ['jshint'], function(cb){ - console.log('Watching files for changes...'); - gulp.watch(jsFiles.concat(jsxFiles), ['jshint']); -}); - -gulp.task('default', ['jshint']); diff --git a/images/happy_device.png b/images/happy_device.png new file mode 100644 index 0000000000..e040f93daa Binary files /dev/null and b/images/happy_device.png differ diff --git a/images/unhappy_device.png b/images/unhappy_device.png new file mode 100644 index 0000000000..4ccf3b5a62 Binary files /dev/null and b/images/unhappy_device.png differ diff --git a/images/unhappy_meter.png b/images/unhappy_meter.png new file mode 100644 index 0000000000..7dd5dfd6fe Binary files /dev/null and b/images/unhappy_meter.png differ diff --git a/index.html b/index.html index 35f43e556e..1ce8903cd9 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,7 @@ Tidepool Uploader +
diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000000..c35b1eee6d --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,24 @@ +var webpack = require('webpack'); +var webpackConf = require('./test.config.js'); + +module.exports = function (config) { + config.set({ + browsers: [ 'PhantomJS' ], + captureTimeout: 60000, + browserNoActivityTimeout: 60000, // We need to accept that Webpack may take a while to build! + singleRun: true, + colors: true, + frameworks: [ 'mocha', 'chai' ], // Mocha is our testing framework of choice + files: [ + 'browser.tests.js' + ], + preprocessors: { + 'browser.tests.js': [ 'webpack' ] // Preprocess with webpack and our sourcemap loader + }, + reporters: [ 'mocha' ], + webpack: webpackConf, + webpackServer: { + noInfo: true // We don't want webpack output + } + }); +}; \ No newline at end of file diff --git a/lib/TimezoneOffsetUtil.js b/lib/TimezoneOffsetUtil.js index f3d782d605..0f7a774a59 100644 --- a/lib/TimezoneOffsetUtil.js +++ b/lib/TimezoneOffsetUtil.js @@ -15,8 +15,6 @@ * == BSD2 LICENSE == */ -'use strict'; - var _ = require('lodash'); var util = require('util'); @@ -146,7 +144,7 @@ module.exports = function(timezone, mostRecent, changes) { } debug('Computed offset intervals', offsetIntervals); this.records = changes; - this.lookup = function() { + this.lookup = (function() { if (!_.isEmpty(offsetIntervals)) { return function(datetime, index) { for (var i = 0; i < offsetIntervals.length; ++i) { @@ -205,7 +203,7 @@ module.exports = function(timezone, mostRecent, changes) { }; }; } - }(); + })(); this.fillInUTCInfo = function(obj, jsDate) { if (typeof obj !== 'object' || Array.isArray(obj)) { diff --git a/lib/bows.js b/lib/bows.js index 955785b5e9..6f4c4a33aa 100644 --- a/lib/bows.js +++ b/lib/bows.js @@ -39,6 +39,8 @@ function checkColorSupport() { return chrome || firefoxVersion >= 31.0; } +var hue; + var yieldColor = function() { var goldenRatio = 0.618033988749895; hue += goldenRatio; diff --git a/lib/carelink/parsing.js b/lib/carelink/parsing.js index 070e349612..5b21d634a3 100644 --- a/lib/carelink/parsing.js +++ b/lib/carelink/parsing.js @@ -14,7 +14,6 @@ * not, you can obtain one from Tidepool Project at tidepool.org. * == BSD2 LICENSE == */ -'use strict'; var util = require('util'); diff --git a/lib/components/.jshintrc b/lib/components/.jshintrc deleted file mode 100644 index 33a83f52ab..0000000000 --- a/lib/components/.jshintrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "browser": true, - "esnext": true, - "eqnull": true, - "globalstrict": true, - "indent": 2, - "laxcomma": true, - "loopfunc": true, - "node": true, - "quotmark": true, - "smarttabs": false, - "strict": false, - "sub": true, - "trailing": true, - "undef": true, - - "quotmark": false -} diff --git a/lib/components/App.jsx b/lib/components/App.jsx deleted file mode 100644 index 9372400aa0..0000000000 --- a/lib/components/App.jsx +++ /dev/null @@ -1,212 +0,0 @@ -/* - * == BSD2 LICENSE == - * Copyright (c) 2014, Tidepool Project - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the associated License, which is identical to the BSD 2-Clause - * License as published by the Open Source Initiative at opensource.org. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the License for more details. - * - * You should have received a copy of the License along with this program; if - * not, you can obtain one from Tidepool Project at tidepool.org. - * == BSD2 LICENSE == - */ - -var _ = require('lodash'); -var React = require('react'); -var appState = require('../state/appState'); -var appActions = require('../state/appActions'); - -var Loading = require('./Loading.jsx'); -var Login = require('./Login.jsx'); -var LoggedInAs = require('./LoggedInAs.jsx'); -var Scan = require('./Scan.jsx'); -var UploadList = require('./UploadList.jsx'); -var ViewDataLink = require('./ViewDataLink.jsx'); -var UploadSettings = require('./UploadSettings.jsx'); -var TimezoneSelection = require('./TimezoneSelection.jsx'); -var DeviceSelection = require('./DeviceSelection.jsx'); -var UpdatePlease = require('./UpdatePlease.jsx'); - -var config = require('../config'); - -var App = React.createClass({ - getInitialState: function() { - return appState.getInitial(); - }, - - componentWillMount: function() { - appState.bindApp(this); - appActions.bindApp(this); - - appActions.load(_.noop); - - this.appState = appState; - this.appActions = appActions; - this.localStore = require('../core/localStore'); - this.api = require('../core/api'); - this.device = require('../core/device'); - }, - - onlyMe: function() { - var self = this; - return (!_.isEmpty(self.state.user.uploadGroups) && this.state.user.uploadGroups.length === 1); - }, - - render: function() { - return ( -
-
{this.renderHeader()}
-
{this.renderPage()}
-
{this.renderFooter()}
-
- ); - }, - - renderHeader: function() { - if (this.state.page === 'loading') { - return null; - } - - if (!this.appState.isLoggedIn()) { - return this.renderSignupLink(); - } - - return ; - }, - - renderPage: function() { - var page = this.state.page; - var targetTimezone = this.state.targetTimezone; - - if (page === 'loading') { - return ; - } - - if (page === 'login') { - return ; - } - - var uploadSettings = this.onlyMe() ? null : this.renderUploadSettings(); - var timezone = this.renderTimezoneSelection(); - - if (page === 'settings') { - return ( -
- {uploadSettings} - {timezone} - -
- ); - } - - if (page === 'main') { - return ( -
- {uploadSettings} - - {this.renderViewDataLink()} -
- ); - } - - if (page === 'error') { - return ( - // TODO: add the link to help page on tidepool.org or knowledge base - // re: how to update the uploader - - ); - } - - return null; - }, - - renderFooter: function() { - return( -
- -
{'v'+config.version+' beta'}
-
- ); - }, - - renderSignupLink: function() { - return ( -
- - Sign up -
- ); - }, - - renderScan: function() { - if (this.appState.hasUploadInProgress()) { - return null; - } - - return ; - }, - - renderUploadSettings: function() { - return ( - - ); - }, - - renderTimezoneSelection: function() { - return ( - - ); - }, - - renderViewDataLink: function() { - return ; - }, - - renderAppState: function() { - return ( -
-
-
{JSON.stringify(this.state, null, 2)}
-
- ); - } -}); - -module.exports = App; diff --git a/lib/components/DevTools.js b/lib/components/DevTools.js new file mode 100644 index 0000000000..417d00ef2e --- /dev/null +++ b/lib/components/DevTools.js @@ -0,0 +1,30 @@ +/* + * == BSD2 LICENSE == + * Copyright (c) 2015, Tidepool Project + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the associated License, which is identical to the BSD 2-Clause + * License as published by the Open Source Initiative at opensource.org. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the License for more details. + * + * You should have received a copy of the License along with this program; if + * not, you can obtain one from Tidepool Project at tidepool.org. + * == BSD2 LICENSE == + */ + +import React from 'react'; +import { createDevTools } from 'redux-devtools'; +import LogMonitor from 'redux-devtools-log-monitor'; +import DockMonitor from 'redux-devtools-dock-monitor'; + +export default createDevTools( + + + +); diff --git a/lib/components/DeviceSelection.js b/lib/components/DeviceSelection.js new file mode 100644 index 0000000000..2825ef4632 --- /dev/null +++ b/lib/components/DeviceSelection.js @@ -0,0 +1,131 @@ +/* +* == BSD2 LICENSE == +* Copyright (c) 2014, Tidepool Project +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the associated License, which is identical to the BSD 2-Clause +* License as published by the Open Source Initiative at opensource.org. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the License for more details. +* +* You should have received a copy of the License along with this program; if +* not, you can obtain one from Tidepool Project at tidepool.org. +* == BSD2 LICENSE == +*/ + +var _ = require('lodash'); +var React = require('react'); +var cx = require('classnames'); + +import { urls } from '../redux/constants/otherConstants'; + +var DeviceSelection = React.createClass({ + propTypes: { + disabled: React.PropTypes.bool.isRequired, + devices: React.PropTypes.object.isRequired, + os: React.PropTypes.string.isRequired, + targetDevices: React.PropTypes.array.isRequired, + // targetId can be null when logged in user is not a data storage account + // for example a clinic worker + targetId: React.PropTypes.string, + timezoneIsSelected: React.PropTypes.bool.isRequired, + userDropdownShowing: React.PropTypes.bool.isRequired, + userIsSelected: React.PropTypes.bool.isRequired, + addDevice: React.PropTypes.func.isRequired, + removeDevice: React.PropTypes.func.isRequired, + onDone: React.PropTypes.func.isRequired + }, + + componentWillReceiveProps: function(nextProps) { + var self = this; + + if (!this.props.userIsSelected && nextProps.userIsSelected) { + _.each(self.props.targetDevices, function(device) { + self.props.addDevice(nextProps.targetId, device); + }); + } + }, + + render: function() { + var targetUser = this.props.targetId || 'noUserSelected'; + var addDevice = this.props.addDevice.bind(null, targetUser); + var removeDevice = this.props.removeDevice.bind(null, targetUser); + var devices = this.props.devices; + + var onCheckedChange = function(e) { + if (e.target.checked) { + addDevice(e.target.value); + } + else { + removeDevice(e.target.value); + } + }; + var os = this.props.os; + var targetDevices = this.props.targetDevices; + + var items = _.map(devices, function(device) { + var isChecked = _.contains(targetDevices, device.key); + var driverLink = ''; + + if (device.showDriverLink[os] === true && + device.enabled[os] === true) { + driverLink = urls.DRIVER_DOWNLOAD; + } + + var driverLinkDisplay = null; + if (isChecked && !_.isEmpty(driverLink)) { + driverLinkDisplay = ( + + ); + } + return ( +
+
+ + +
+ {driverLinkDisplay} +
+ ); + }); + + var formClasses = cx({ + 'DeviceSelection-form': true, + 'DeviceSelection-form--onlyme': !this.props.userDropdownShowing, + 'DeviceSelection-form--groups': this.props.userDropdownShowing, + 'DeviceSelection-form--timezone' : true + }); + + var disabled = (this.props.targetDevices.length > 0 && + this.props.timezoneIsSelected && + this.props.userIsSelected) && + !this.props.disabled ? + false : true; + return ( +
+

Choose devices

+
{items}
+ +
+ ); + }, + + handleSubmit: function() { + this.props.onDone(); + } +}); + +module.exports = DeviceSelection; diff --git a/lib/components/DeviceSelection.jsx b/lib/components/DeviceSelection.jsx deleted file mode 100644 index ff28ce46b7..0000000000 --- a/lib/components/DeviceSelection.jsx +++ /dev/null @@ -1,100 +0,0 @@ -/* -* == BSD2 LICENSE == -* Copyright (c) 2014, Tidepool Project -* -* This program is free software; you can redistribute it and/or modify it under -* the terms of the associated License, which is identical to the BSD 2-Clause -* License as published by the Open Source Initiative at opensource.org. -* -* This program is distributed in the hope that it will be useful, but WITHOUT -* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -* FOR A PARTICULAR PURPOSE. See the License for more details. -* -* You should have received a copy of the License along with this program; if -* not, you can obtain one from Tidepool Project at tidepool.org. -* == BSD2 LICENSE == -*/ - -var _ = require('lodash'); -var React = require('react'); -var cx = require('react/lib/cx'); - -var DeviceSelection = React.createClass({ - propTypes: { - uploads: React.PropTypes.array.isRequired, - // targetId can be null when logged in user is not a data storage account - // for example a clinic worker - targetId: React.PropTypes.string, - targetDevices: React.PropTypes.array.isRequired, - timezoneIsSelected: React.PropTypes.bool.isRequired, - onCheckChange: React.PropTypes.func.isRequired, - onDone: React.PropTypes.func.isRequired, - groupsDropdown: React.PropTypes.bool.isRequired - }, - - render: function() { - var self = this; - - var items = _.map(this.props.uploads, function(upload) { - - var isChecked = _.contains(self.props.targetDevices, upload.key); - var driverLink = ''; - - if((window.app.state._os === 'mac') && (upload.mac !== undefined) ) { - driverLink = upload.mac.driverLink; - } - - if((window.app.state._os === 'win') && (upload.win !== undefined) ) { - driverLink = upload.win.driverLink; - } - - var displayText = ''; - if (isChecked && !_.isEmpty(driverLink)) { - displayText = ; - } - return ( -
-
- - -
- {displayText} -
- ); - }); - - var formClasses = cx({ - 'DeviceSelection-form': true, - 'DeviceSelection-form--onlyme': !this.props.groupsDropdown, - 'DeviceSelection-form--groups': this.props.groupsDropdown, - 'DeviceSelection-form--timezone' : true - }); - - var disabled = (this.props.targetDevices.length > 0 && this.props.timezoneIsSelected) ? - false : true; - return ( -
-

Choose devices

-
{items}
- -
- ); - }, - - handleSubmit: function() { - this.props.onDone(this.props.targetId); - } -}); - -module.exports = DeviceSelection; diff --git a/lib/components/Loading.jsx b/lib/components/Loading.js similarity index 100% rename from lib/components/Loading.jsx rename to lib/components/Loading.js diff --git a/lib/components/LoadingBar.jsx b/lib/components/LoadingBar.js similarity index 100% rename from lib/components/LoadingBar.jsx rename to lib/components/LoadingBar.js diff --git a/lib/components/LoggedInAs.jsx b/lib/components/LoggedInAs.js similarity index 71% rename from lib/components/LoggedInAs.jsx rename to lib/components/LoggedInAs.js index 608adc9749..4f303548f2 100644 --- a/lib/components/LoggedInAs.jsx +++ b/lib/components/LoggedInAs.js @@ -14,17 +14,18 @@ * not, you can obtain one from Tidepool Project at tidepool.org. * == BSD2 LICENSE == */ - +var _ = require('lodash'); var React = require('react'); var getIn = require('../core/getIn'); var LoggedInAs = React.createClass({ propTypes: { dropMenu: React.PropTypes.bool.isRequired, - user: React.PropTypes.object.isRequired, - onClicked: React.PropTypes.func.isRequired, + isUploadInProgress: React.PropTypes.bool.isRequired, onChooseDevices: React.PropTypes.func.isRequired, - onLogout: React.PropTypes.func.isRequired + onClicked: React.PropTypes.func.isRequired, + onLogout: React.PropTypes.func.isRequired, + user: React.PropTypes.object }, getInitialState: function() { @@ -35,11 +36,12 @@ var LoggedInAs = React.createClass({ render: function() { var dropMenu = this.props.dropMenu ? this.renderDropMenu() : null; + var user = this.props.user; return (
- {this.getName()} + {_.get(user, 'fullName', '')}
{dropMenu} @@ -62,7 +64,23 @@ var LoggedInAs = React.createClass({ }, renderChooseDevices: function() { - return Choose Devices; + var uploadInProgress = this.props.isUploadInProgress; + + var noopHandler = function(e) { + if (e) { + e.preventDefault(); + } + }; + return ( + + + Choose Devices + + ); }, renderLogout: function() { @@ -70,7 +88,14 @@ var LoggedInAs = React.createClass({ return Logging out...; } - return Logout; + return ( + + + Logout + + ); }, getName: function() { diff --git a/lib/components/Login.jsx b/lib/components/Login.js similarity index 71% rename from lib/components/Login.jsx rename to lib/components/Login.js index ab64a11d62..7dacdc950f 100644 --- a/lib/components/Login.jsx +++ b/lib/components/Login.js @@ -21,16 +21,13 @@ var config = require('../config'); var Login = React.createClass({ propTypes: { + disabled: React.PropTypes.bool.isRequired, + errorMessage: React.PropTypes.string, + forgotPasswordUrl: React.PropTypes.string.isRequired, + isFetching: React.PropTypes.bool.isRequired, onLogin: React.PropTypes.func.isRequired }, - getInitialState: function() { - return { - working: false, - error: null - }; - }, - render: function() { return (
@@ -61,27 +58,24 @@ var Login = React.createClass({ renderForgotPasswordLink: function() { return ( - + {'Forgot your password?'} ); }, renderButton: function() { - var disabled; var text = 'Login'; - if (this.state.working) { - disabled = true; + if (this.props.isFetching) { text = 'Logging in...'; } return ( - ); @@ -89,35 +83,22 @@ var Login = React.createClass({ handleLogin: function(e) { e.preventDefault(); - var username = this.refs.username.getDOMNode().value; - var password = this.refs.password.getDOMNode().value; - var remember = this.refs.remember.getDOMNode().checked; + var username = this.refs.username.value; + var password = this.refs.password.value; + var remember = this.refs.remember.checked; - this.setState({ - working: true, - error: null - }); - var self = this; - this.props.onLogin({ - username: username, - password: password - }, {remember: remember}, function(err) { - if (err) { - self.setState({ - working: false, - error: 'Wrong username or password.' - }); - return; - } - }); + this.props.onLogin( + {username: username, password: password}, + {remember: remember} + ); }, renderError: function() { - if (!this.state.error) { + if (!this.props.errorMessage) { return null; } - return {this.state.error}; + return {this.props.errorMessage}; } }); diff --git a/lib/components/ProgressBar.jsx b/lib/components/ProgressBar.js similarity index 100% rename from lib/components/ProgressBar.jsx rename to lib/components/ProgressBar.js diff --git a/lib/components/Scan.jsx b/lib/components/Scan.jsx deleted file mode 100644 index e76b22cd70..0000000000 --- a/lib/components/Scan.jsx +++ /dev/null @@ -1,100 +0,0 @@ -/* -* == BSD2 LICENSE == -* Copyright (c) 2014, Tidepool Project -* -* This program is free software; you can redistribute it and/or modify it under -* the terms of the associated License, which is identical to the BSD 2-Clause -* License as published by the Open Source Initiative at opensource.org. -* -* This program is distributed in the hope that it will be useful, but WITHOUT -* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -* FOR A PARTICULAR PURPOSE. See the License for more details. -* -* You should have received a copy of the License along with this program; if -* not, you can obtain one from Tidepool Project at tidepool.org. -* == BSD2 LICENSE == -*/ - -var _ = require('lodash'); -var React = require('react'); -var bows = require('../bows'); -var repeat = require('../core/repeat'); - -var DETECT_DELAY = 120000; // Scan every X milliseconds -var DETECT_TIMEOUT = null; // Scan forever - -var Scan = React.createClass({ - propTypes: { - onDetectDevices: React.PropTypes.func.isRequired - }, - - log: bows('Scan'), - - getInitialState: function() { - return { - scanning: true, - error: null - }; - }, - - stopScanning: _.noop, - - componentDidMount: function() { - this.log('Start scanning for devices...'); - setTimeout(this.startScanning, 1000); - }, - - render: function() { - return ( -
- {this.renderError()} - {this.renderScan()} -
- ); - }, - - renderError: function() { - if (!this.state.error) { - return null; - } - - return ( -
- {'An error occurred while scanning for devices.'} -
- ); - }, - - renderScan: function() { - if (this.state.scanning) { - return null; - } - return ( -
- -
- ); - }, - - startScanning: function() { - this.setState({ - scanning: true, - error: null - }); - - this.stopScanning = repeat( - this.props.onDetectDevices, DETECT_DELAY, DETECT_TIMEOUT, this.handleScanEnd - ); - }, - - handleScanEnd: function(err) { - this.setState({ - scanning: false, - error: err - }); - } -}); - -module.exports = Scan; diff --git a/lib/components/TimezoneSelection.jsx b/lib/components/TimezoneDropdown.js similarity index 62% rename from lib/components/TimezoneSelection.jsx rename to lib/components/TimezoneDropdown.js index 3b7aca32ee..e202b1f95b 100644 --- a/lib/components/TimezoneSelection.jsx +++ b/lib/components/TimezoneDropdown.js @@ -20,13 +20,29 @@ var React = require('react'); var sundial = require('sundial'); var Select = require('react-select'); -var TimezoneSelection = React.createClass({ +var TimezoneDropdown = React.createClass({ propTypes: { onTimezoneChange: React.PropTypes.func.isRequired, - timezoneLabel : React.PropTypes.string.isRequired, + selectorLabel: React.PropTypes.string.isRequired, + // targetId can be null when logged in user is not a data storage account + // for example a clinic worker + targetId: React.PropTypes.string, targetTimezone: React.PropTypes.string }, + componentWillReceiveProps: function(nextProps) { + var self = this; + + if (!this.props.targetId && nextProps.targetId !== null) { + if (this.props.targetTimezone !== null) { + this.props.onTimezoneChange( + nextProps.targetId, + this.props.targetTimezone + ); + } + } + }, + buildTzSelector: function() { var self = this; function sortByOffset(timezones) { @@ -39,11 +55,12 @@ var TimezoneSelection = React.createClass({ .concat(sortByOffset(timezones.unitedStates)) .concat(sortByOffset(timezones.hoisted)) .concat(sortByOffset(timezones.theRest)); + var targetUser = this.props.targetId || 'noUserSelected'; return ( +
+ ); + } + + renderButton() { + const { text, upload } = this.props; + let labelText = text.LABEL_UPLOAD; + let disabled = upload.disabled || this.props.disabled; + + if (_.get(upload, 'source.type', null) === 'carelink') { + labelText = text.LABEL_IMPORT; + disabled = disabled || this.state.carelinkFormIncomplete; + } + + if (_.get(upload, 'source.type', null) === 'block') { + return null; + } + + return ( +
+ +
+ ); + } + + renderCareLinkInputs() { + const { upload } = this.props; + if (_.get(upload, 'source.type', null) !== 'carelink') { + return null; + } + + return ( +
+
+ +
+
+ +
+
+ ); + } + + renderInstructions() { + const { upload } = this.props; + let details = upload.instructions || ''; + if (Array.isArray(details)) { + return ( +
+ {_.get(details, 0, '')} + {_.get(details, 1, '')} +
+ ); + } + return ( +
{details}
+ ); + } + + renderLastUpload() { + const { upload } = this.props; + let history = upload.history; + + if (!(history && history.length)) { + return null; + } + + let lastUpload = _.find(history, function(upload) { + return upload.finish && !upload.error; + }); + + if (lastUpload == null) { + return null; + } + + let time = sundial.formatCalendarTime(lastUpload.finish); + return ( +
{this.props.text.LAST_UPLOAD + time}
+ ); + } + + renderName() { + const { upload, text } = this.props; + return ( +
{upload.name || text.DEVICE_UNKOWN}
+ ); + } + + renderProgress() { + const { upload } = this.props; + if (upload.failed) { + return
; + } + + if (this.isFetchingCareLinkData()) { + return
; + } + + let percentage = upload.progress && upload.progress.percentage; + + // can be equal to 0, so check for null or undefined + if (percentage == null) { + return null; + } + + return
; + } + + renderReset() { + const { upload } = this.props; + if (!upload.completed) { + return null; + } + let resetClass = cx({ + 'Upload-reset': true, + 'Upload-reset--error': upload.failed, + 'Upload-reset--success': upload.successful + }); + + let text = upload.successful ? + this.props.text.LABEL_OK : this.props.text.LABEL_FAILED; + + return ( +
+ {text} +
+ ); + } + + renderStatus() { + const { upload } = this.props; + if (this.isFetchingCareLinkData()) { + return
{this.props.text.CARELINK_DOWNLOADING}
; + } + + if (upload.uploading) { + return
{this.props.text.UPLOAD_PROGRESS + this.props.upload.progress.percentage + '%'}
; + } + + if (upload.successful) { + let dataDownloadLink = null; + if (__DEBUG__ && (!_.isEmpty(this.props.upload.data) && Array.isArray(this.props.upload.data))) { + let filename = 'uploader-processed-records.json'; + let jsonData = JSON.stringify(this.props.upload.data, undefined, 4); + let blob = new Blob([jsonData], {type: 'text/json'}); + let dataHref = URL.createObjectURL(blob); + dataDownloadLink = ( + + Download POST data + + ); + } + return
{this.props.text.UPLOAD_COMPLETE} {dataDownloadLink}
; + } + + if (this.isBlockModeFileChosen()) { + return ( +
+
Preparing file …
+
{this.props.upload.file.name}
+
+ ); + } + return null; + } + + isBlockModeFileChosen() { + const { upload } = this.props; + if (_.get(upload, 'source.type', null) !== 'block') { + return false; + } + else { + if (!_.isEmpty(_.get(upload, 'file.name', ''))) { + return true; + } + return false; + } + } + + isFetchingCareLinkData() { + const { upload } = this.props; + return (_.get(upload, 'source.type', null) === 'carelink') && + (upload.isFetching); + } +}; diff --git a/lib/components/Upload.jsx b/lib/components/Upload.jsx deleted file mode 100644 index 0aeba3dc44..0000000000 --- a/lib/components/Upload.jsx +++ /dev/null @@ -1,372 +0,0 @@ -/* -* == BSD2 LICENSE == -* Copyright (c) 2014, Tidepool Project -* -* This program is free software; you can redistribute it and/or modify it under -* the terms of the associated License, which is identical to the BSD 2-Clause -* License as published by the Open Source Initiative at opensource.org. -* -* This program is distributed in the hope that it will be useful, but WITHOUT -* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -* FOR A PARTICULAR PURPOSE. See the License for more details. -* -* You should have received a copy of the License along with this program; if -* not, you can obtain one from Tidepool Project at tidepool.org. -* == BSD2 LICENSE == -*/ - -var _ = require('lodash'); -var React = require('react'); -var sundial = require('sundial'); -var getIn = require('../core/getIn'); -var deviceInfo = require('../core/deviceInfo'); -var ProgressBar = require('./ProgressBar.jsx'); -var LoadingBar = require('./LoadingBar.jsx'); - -var Upload = React.createClass({ - propTypes: { - upload: React.PropTypes.object.isRequired, - onUpload: React.PropTypes.func.isRequired, - onReset: React.PropTypes.func.isRequired, - readFile: React.PropTypes.func.isRequired, - text: React.PropTypes.object - }, - - getInitialState: function() { - return { - carelinkFormIncomplete: true, - blockModeFileNotChosen: true - }; - }, - getDefaultProps: function(){ - return { - text: { - CARELINK_CREDS_NOT_SAVED :'Import from CareLink
(We will not store your credentials)', - CARELINK_USERNAME :'CareLink username', - CARELINK_PASSWORD :'CareLink password', - CARELINK_DOWNLOADING :'Downloading CareLink export...', - LABEL_UPLOAD : 'Upload', - LABEL_IMPORT : 'Import', - LABEL_OK : 'OK', - LABEL_FAILED: 'Try again', - LAST_UPLOAD : 'Last upload: ', - LABEL_MEDTRONIC_DEVICES :'Medtronic', - DEVICE_UNKOWN : 'Unknown device', - UPLOAD_COMPLETE: 'Done!', - UPLOAD_PROGRESS: 'Uploading... ' - } - }; - }, - render: function() { - return ( -
-
- {this.renderName()} - {this.renderDetail()} - {this.renderLastUpload()} -
-
-
- {this.renderStatus()} -
- {this.renderProgress()} - {this.renderActions()} -
-
- ); - }, - renderName: function() { - var name; - if (this.isCarelinkUpload()) { - name = this.props.text.LABEL_MEDTRONIC_DEVICES; - } - else { - name = this.getDeviceName(this.props.upload); - } - return ( -
{name}
- ); - }, - renderDetail: function() { - var detail; - if (this.isCarelinkUpload()) { - detail = this.props.text.CARELINK_CREDS_NOT_SAVED; - } - else { - detail = this.getDeviceDetail(this.props.upload); - } - return ( -
- ); - }, - renderActions: function() { - if (this.isUploading()) { - return null; - } - - if (this.isUploadCompleted() || this.isDisconnected()) { - return ( -
- {this.renderReset()} -
- ); - } - - return ( -
- {this.renderCarelinkInputs()} - {this.renderBlockModeInput()} - {this.renderButton()} -
- ); - }, - renderBlockModeInput: function() { - if (!this.isBlockModeDevice()) { - return null; - } - - // don't show the 'choose file' button if a file has already been selected. - if (this.isBlockModeFileChosen()) { - return null; - } - - return ( -
- -
- ); - }, - onBlockModeInputChange: function(e) { - var file = e.target.files[0]; - var fileResult = this.props.readFile(file, this.props.upload.source.extension); - this.setState({ - blockModeFileNotChosen: fileResult === true ? false : true - }); - }, - renderCarelinkInputs: function() { - if (!this.isCarelinkUpload()) { - return null; - } - - return ( -
-
-
-
- ); - }, - onCareLinkInputChange: function() { - var username = this.refs.username && this.refs.username.getDOMNode().value; - var password = this.refs.password && this.refs.password.getDOMNode().value; - - if (!username || !password) { - this.setState({carelinkFormIncomplete: true}); - } else { - this.setState({carelinkFormIncomplete: false}); - } - }, - renderButton: function() { - var text = this.props.text.LABEL_UPLOAD; - var disabled = this.isDisabled(); - - if (this.isCarelinkUpload()) { - text = this.props.text.LABEL_IMPORT; - disabled = disabled || this.state.carelinkFormIncomplete; - } - if (this.isBlockModeDevice()) { - return null; - } - - return ( -
- -
- ); - }, - renderProgress: function() { - if (this.isUploadFailed()) { - return
; - } - - if (this.isFetchingCarelinkData()) { - return
; - } - - var percentage = - this.props.upload.progress && this.props.upload.progress.percentage; - - // Can be equal to 0, so check for null or undefined - if (percentage == null) { - return null; - } - - return
; - }, - renderStatus: function() { - if (this.isDisconnected()) { - return ( -
- {'Connect your ' + this.getDeviceName(this.props.upload) + '...'} -
- ); - } - if (this.isFetchingCarelinkData()) { - return
{this.props.text.CARELINK_DOWNLOADING}
; - } - if (this.isUploading()) { - return
{this.props.text.UPLOAD_PROGRESS + this.props.upload.progress.percentage + '%'}
; - } - if (this.isUploadSuccessful()) { - return
{this.props.text.UPLOAD_COMPLETE}
; - } - if (this.isBlockModeFileChosen()) { - return ( -
-
Preparing file …
-
{this.props.upload.file.name}
-
- ); - } - return null; - }, - renderReset: function() { - if (!this.isUploadCompleted()) { - return null; - } - - var text = this.isUploadSuccessful() ? this.props.text.LABEL_OK : this.props.text.LABEL_FAILED; - var classes = 'Upload-reset'; - if (this.isUploadFailed()) { - text = this.props.text.LABEL_FAILED; - classes = classes + ' Upload-reset--error'; - } - else { - classes = classes + ' Upload-reset--success'; - } - - return ( -
- {text} -
- ); - }, - renderLastUpload: function() { - var lastUpload = this.getLastUpload(); - if (!lastUpload || lastUpload.error != null) { - return null; - } - var time = sundial.formatCalendarTime(lastUpload.finish); - return
{this.props.text.LAST_UPLOAD + time}
; - }, - getLastUpload: function() { - var history = this.props.upload.history; - if (!(history && history.length)) { - return null; - } - return history[0]; - }, - getDeviceName: function(upload) { - var text = this.props.text; - var getName = getIn( - deviceInfo, - [upload.source.driverId, 'getName'], - function() { return text.DEVICE_UNKOWN; } - ); - return getName(upload.source); - }, - getDeviceDetail: function(upload) { - var getDetail = getIn( - deviceInfo, - [upload.source.driverId, 'getDetail'], - function() { return ''; } - ); - return getDetail(upload.source); - }, - - getUploadError: function() { - return this.props.upload.error; - }, - - isDisabled: function() { - return this.props.upload.disabled; - }, - - isDisconnected: function() { - return this.props.upload.disconnected; - }, - - isUploading: function() { - return this.props.upload.uploading; - }, - - isBlockModeDevice: function() { - return this.props.upload.source.type === 'block'; - }, - - isBlockModeFileChosen: function() { - if (this.state.blockModeFileNotChosen) { - return false; - } - else { - if (this.props.upload.source.type === 'block') { - return this.props.upload.file && !_.isEmpty(this.props.upload.file.name); - } - } - }, - - isCarelinkUpload: function() { - return this.props.upload.carelink; - }, - - isFetchingCarelinkData: function() { - return this.props.upload.fetchingCarelinkData; - }, - - isUploadSuccessful: function() { - return this.props.upload.successful; - }, - - isUploadFailed: function() { - return this.props.upload.failed; - }, - - isUploadCompleted: function() { - return this.props.upload.completed; - }, - - handleUpload: function(e) { - if (e) { - e.preventDefault(); - } - - if (this.isCarelinkUpload()) { - return this.handleCarelinkUpload(); - } - - var options = {}; - this.props.onUpload(options); - }, - - handleCarelinkUpload: function() { - var username = this.refs.username.getDOMNode().value; - var password = this.refs.password.getDOMNode().value; - var options = { - username: username, - password: password - }; - this.props.onUpload(options); - }, - - handleReset: function(e) { - if (e) { - e.preventDefault(); - } - - this.props.onReset(); - } -}); - -module.exports = Upload; diff --git a/lib/components/UploadList.js b/lib/components/UploadList.js new file mode 100644 index 0000000000..cb816544a8 --- /dev/null +++ b/lib/components/UploadList.js @@ -0,0 +1,121 @@ + +/* +* == BSD2 LICENSE == +* Copyright (c) 2016, Tidepool Project +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the associated License, which is identical to the BSD 2-Clause +* License as published by the Open Source Initiative at opensource.org. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the License for more details. +* +* You should have received a copy of the License along with this program; if +* not, you can obtain one from Tidepool Project at tidepool.org. +* == BSD2 LICENSE == +*/ + +import _ from 'lodash'; +import React, { Component, PropTypes } from 'react'; +import cx from 'classnames'; + +import Upload from './Upload'; + +export default class UploadList extends Component { + static propTypes = { + disabled: PropTypes.bool.isRequired, + // targetId can be null when logged in user is not a data storage account + // for example a clinic worker + targetId: PropTypes.string, + uploads: PropTypes.array.isRequired, + userDropdownShowing: PropTypes.bool.isRequired, + onReset: PropTypes.func.isRequired, + onUpload: PropTypes.func.isRequired, + readFile: PropTypes.func.isRequired, + toggleErrorDetails: PropTypes.func.isRequired + }; + + static defaultProps = { + text: { + SHOW_ERROR : 'Error details', + HIDE_ERROR : 'Hide details', + UPLOAD_FAILED : 'Upload Failed: ' + } + }; + + constructor(props) { + super(props); + } + + render() { + const uploadListClasses = cx({ + UploadList: true, + 'UploadList--onlyme': !this.props.userDropdownShowing, + 'UploadList--selectuser': this.props.userDropdownShowing + }); + + const { devices, disabled, onReset, onUpload, targetId } = this.props; + + const items = _.map(this.props.uploads, (upload) => { + return ( +
+ + {this.renderErrorForUpload(upload)} +
+ ); + }); + + return ( +
+ {items} +
+ ); + } + + renderErrorForUpload(upload) { + const { targetId, toggleErrorDetails } = this.props; + if (_.isEmpty(upload) || _.isEmpty(upload.error)) { + return null; + } + const errorDetails = upload.showErrorDetails ? + (
{upload.error.debug}
) : null; + const errorMessage = upload.error.driverLink ? + ( +
+ {this.props.text.UPLOAD_FAILED} + {'It\'s possible you need to install the '} + Tidepool USB driver +
+ ) : + ( +
+ {this.props.text.UPLOAD_FAILED} + {upload.error.message} +
+ ); + const showErrorsText = upload.showErrorDetails ? this.props.text.HIDE_ERROR : this.props.text.SHOW_ERROR; + + function makeToggleDetailsFn() { + return function(e) { + if (e) { + e.preventDefault(); + } + toggleErrorDetails(targetId, upload.key, upload.showErrorDetails); + }; + } + + return ( +
+ {errorMessage} + + {errorDetails} +
+ ); + } +} \ No newline at end of file diff --git a/lib/components/UploadList.jsx b/lib/components/UploadList.jsx deleted file mode 100644 index 889215a316..0000000000 --- a/lib/components/UploadList.jsx +++ /dev/null @@ -1,131 +0,0 @@ -/* -* == BSD2 LICENSE == -* Copyright (c) 2014, Tidepool Project -* -* This program is free software; you can redistribute it and/or modify it under -* the terms of the associated License, which is identical to the BSD 2-Clause -* License as published by the Open Source Initiative at opensource.org. -* -* This program is distributed in the hope that it will be useful, but WITHOUT -* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -* FOR A PARTICULAR PURPOSE. See the License for more details. -* -* You should have received a copy of the License along with this program; if -* not, you can obtain one from Tidepool Project at tidepool.org. -* == BSD2 LICENSE == -*/ - -var _ = require('lodash'); -var React = require('react'); -var cx = require('react/lib/cx'); -var Upload = require('./Upload.jsx'); - -var UploadList = React.createClass({ - propTypes: { - uploads: React.PropTypes.array.isRequired, - targetedUploads: React.PropTypes.array.isRequired, - onUpload: React.PropTypes.func.isRequired, - onReset: React.PropTypes.func.isRequired, - readFile: React.PropTypes.func.isRequired, - groupsDropdown: React.PropTypes.bool.isRequired, - text: React.PropTypes.object - }, - getInitialState: function() { - return { - showErrorDetails: [] - }; - }, - getDefaultProps: function(){ - return { - text: { - SHOW_ERROR : 'Error details', - HIDE_ERROR : 'Hide details', - UPLOAD_FAILED : 'Upload Failed: ' - } - }; - }, - makeHandleShowDetailsFn: function(upload){ - var self = this; - - return function(e) { - if(e){ - e.preventDefault(); - } - // add or remove this upload's key to the list of uploads to show errors for - var showErrorsList = self.state.showErrorDetails; - if (_.includes(showErrorsList, upload.key)) { - showErrorsList = _.reject(showErrorsList, function(i) { return i === upload.key; }); - } - else { - showErrorsList.push(upload.key); - } - self.setState({showErrorDetails: showErrorsList}); - }; - }, - renderErrorForUpload: function(upload) { - if (_.isEmpty(upload) || _.isEmpty(upload.error)) { - return; - } - var showDetailsThisUpload = _.includes(this.state.showErrorDetails, upload.key); - var errorDetails = showDetailsThisUpload ? (
{upload.error.debug}
) : null; - var showErrorsText = showDetailsThisUpload ? this.props.text.HIDE_ERROR : this.props.text.SHOW_ERROR; - var errorMessage = upload.error.driverLink ?
- {this.props.text.UPLOAD_FAILED} - {upload.error.friendlyMessage} - {upload.error.driverName} -
: - {this.props.text.UPLOAD_FAILED + upload.error.friendlyMessage}; - - var clickHandler = this.makeHandleShowDetailsFn(upload); - - return ( -
- {errorMessage} - - {errorDetails} -
- ); - }, - render: function() { - var self = this; - var uploadListClasses = cx({ - UploadList: true, - 'UploadList--onlyme': !this.props.groupsDropdown, - 'UploadList--groups': this.props.groupsDropdown - }); - - var nodes = _.map(this.props.targetedUploads, function(target){ - var keyToMatch; - var index = _.findIndex(self.props.uploads, function(upload) { - if(upload.key === target.key){ - keyToMatch = target.key; - return true; - } - return false; - }); - var matchingUpload = _.find(self.props.targetedUploads, function(upload) { - return upload.key === keyToMatch; - }); - return ( -
- - {self.renderErrorForUpload(matchingUpload)} -
- ); - }); - - return ( -
-
- {nodes} -
-
- ); - } -}); - -module.exports = UploadList; diff --git a/lib/components/UploadSettings.jsx b/lib/components/UserDropdown.js similarity index 51% rename from lib/components/UploadSettings.jsx rename to lib/components/UserDropdown.js index 03946504ea..6485ec8f42 100644 --- a/lib/components/UploadSettings.jsx +++ b/lib/components/UserDropdown.js @@ -19,40 +19,37 @@ var _ = require('lodash'); var React = require('react'); var Select = require('react-select'); -var UploadSettings = React.createClass({ +var pages = require('../redux/constants/otherConstants').pages; + +var UserDropdown = React.createClass({ propTypes: { - page: React.PropTypes.string.isRequired, - user: React.PropTypes.object.isRequired, + allUsers: React.PropTypes.object.isRequired, + isUploadInProgress: React.PropTypes.bool, onGroupChange: React.PropTypes.func.isRequired, + page: React.PropTypes.string.isRequired, targetId: React.PropTypes.string, - isUploadInProgress: React.PropTypes.bool + targetUsersForUpload: React.PropTypes.array.isRequired }, groupSelector: function(){ - // can only upload for yourself - if (_.isEmpty(this.props.user.uploadGroups) || this.props.user.uploadGroups.length <= 1) { - return null; - } - - // only groups we can upload to - // e.g. some people simply aren't `patients` and might be setup without data storage - var available = _.filter(this.props.user.uploadGroups, function(group) { - return _.isEmpty(group.profile.patient) === false; - }); + var allUsers = this.props.allUsers; + var targets = this.props.targetUsersForUpload; // and now return them sorted them by name - var sorted = _.sortBy(available, function(group) { - if (group.profile.patient.isOtherPerson) { - return group.profile.patient.fullName; + var sorted = _.sortBy(targets, function(targetId) { + var targetInfo = allUsers[targetId]; + if (targetInfo.patient.isOtherPerson) { + return targetInfo.patient.fullName; } - return group.profile.fullName; + return targetInfo.fullName; }); - var opts = _.map(sorted, function(group) { - if (group.profile.patient.isOtherPerson) { - return {value: group.userid, label: group.profile.patient.fullName}; + var selectorOpts = _.map(sorted, function(targetId) { + var targetInfo = allUsers[targetId]; + if (targetInfo.patient.isOtherPerson) { + return {value: targetId, label: targetInfo.patient.fullName}; } - return {value: group.userid, label: group.profile.fullName}; + return {value: targetId, label: targetInfo.fullName}; }); var disable = this.props.isUploadInProgress ? true : false; @@ -60,27 +57,29 @@ var UploadSettings = React.createClass({ return (