diff --git a/dashboard/README.md b/dashboard/README.md index 4fc908f38e..a317e140b5 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -68,4 +68,6 @@ The app is structured into presentational components (`src/components`), contain **Provider/consumer components:** These are re-usable components that contain "global" data that needs to be accessible by many (presentational) components in the tree. The provider/consumer pattern is a part of the new [React context API](https://reactjs.org/docs/context.html). -Maintaining this separation will make it easier to migrate to different state management patterns/tools as the app evolves. \ No newline at end of file +Maintaining this separation will make it easier to migrate to different state management patterns/tools as the app evolves. + +We also use the new [React Hooks API](https://reactjs.org/docs/hooks-intro.html) to manage data and state. diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index 06af42add0..c14a4b12f2 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -7056,24 +7056,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true }, "aproba": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "dev": true, "optional": true, "requires": { @@ -7083,12 +7087,14 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -7097,34 +7103,40 @@ }, "chownr": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", "dev": true, "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, "concat-map": { "version": "0.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "2.6.9", - "bundled": true, + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "optional": true, "requires": { @@ -7133,25 +7145,29 @@ }, "deep-extend": { "version": "0.5.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "dev": true, "optional": true, "requires": { @@ -7160,13 +7176,15 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, "requires": { @@ -7182,7 +7200,8 @@ }, "glob": { "version": "7.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "optional": true, "requires": { @@ -7196,13 +7215,15 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.21", - "bundled": true, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", "dev": true, "optional": true, "requires": { @@ -7211,7 +7232,8 @@ }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, "requires": { @@ -7220,7 +7242,8 @@ }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, "requires": { @@ -7230,18 +7253,21 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, "ini": { "version": "1.3.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "requires": { "number-is-nan": "^1.0.0" @@ -7249,13 +7275,15 @@ }, "isarray": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -7263,12 +7291,14 @@ }, "minimist": { "version": "0.0.8", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, "minipass": { "version": "2.2.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, "requires": { "safe-buffer": "^5.1.1", @@ -7277,7 +7307,8 @@ }, "minizlib": { "version": "1.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", "dev": true, "optional": true, "requires": { @@ -7286,7 +7317,8 @@ }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { "minimist": "0.0.8" @@ -7294,13 +7326,15 @@ }, "ms": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true, "optional": true }, "needle": { "version": "2.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", + "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", "dev": true, "optional": true, "requires": { @@ -7311,7 +7345,8 @@ }, "node-pre-gyp": { "version": "0.10.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", "dev": true, "optional": true, "requires": { @@ -7329,7 +7364,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, "requires": { @@ -7339,13 +7375,15 @@ }, "npm-bundled": { "version": "1.0.3", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.1.10", - "bundled": true, + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", "dev": true, "optional": true, "requires": { @@ -7355,7 +7393,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, "requires": { @@ -7367,18 +7406,21 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, "object-assign": { "version": "4.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { "wrappy": "1" @@ -7386,19 +7428,22 @@ }, "os-homedir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, "requires": { @@ -7408,19 +7453,22 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.7", - "bundled": true, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", "dev": true, "optional": true, "requires": { @@ -7432,7 +7480,8 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -7440,7 +7489,8 @@ }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, "requires": { @@ -7455,7 +7505,8 @@ }, "rimraf": { "version": "2.6.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "optional": true, "requires": { @@ -7464,42 +7515,49 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.5.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { "code-point-at": "^1.0.0", @@ -7509,7 +7567,8 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, "requires": { @@ -7518,7 +7577,8 @@ }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -7526,13 +7586,15 @@ }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true }, "tar": { "version": "4.4.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", + "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", "dev": true, "optional": true, "requires": { @@ -7547,13 +7609,15 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "dev": true, "optional": true, "requires": { @@ -7562,12 +7626,14 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, "yallist": { "version": "3.0.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", "dev": true } } @@ -17297,14 +17363,14 @@ } }, "react": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.7.0.tgz", - "integrity": "sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A==", + "version": "16.7.0-alpha.2", + "resolved": "https://registry.npmjs.org/react/-/react-16.7.0-alpha.2.tgz", + "integrity": "sha512-Xh1CC8KkqIojhC+LFXd21jxlVtzoVYdGnQAi/I2+dxbmos9ghbx5TQf9/nDxc4WxaFfUQJkya0w1k6rMeyIaxQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.12.0" + "scheduler": "^0.12.0-alpha.2" } }, "react-app-polyfill": { @@ -17534,14 +17600,14 @@ } }, "react-dom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.7.0.tgz", - "integrity": "sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg==", + "version": "16.7.0-alpha.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.7.0-alpha.2.tgz", + "integrity": "sha512-o0mMw8jBlwHjGZEy/vvKd/6giAX0+skREMOTs3/QHmgi+yAhUClp4My4Z9lsKy3SXV+03uPdm1l/QM7NTcGuMw==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.12.0" + "scheduler": "^0.12.0-alpha.2" } }, "react-error-overlay": { diff --git a/dashboard/package.json b/dashboard/package.json index c705984287..91466a4360 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -11,7 +11,7 @@ "@types/jest": "^23.3.9", "@types/lodash": "^4.14.118", "@types/node": "^10.12.10", - "@types/react": "^16.7.7", + "@types/react": "^16.7.18", "@types/react-dom": "^16.0.11", "@types/react-router-dom": "^4.3.1", "gulp": "^4.0.0", @@ -34,8 +34,8 @@ "lodash": "^4.17.11", "node-sass": "^4.10.0", "normalize-url": "^4.0.0", - "react": "^16.6.3", - "react-dom": "^16.6.3", + "react": "16.7.0-alpha.2", + "react-dom": "16.7.0-alpha.2", "react-router-dom": "^4.3.1", "uuid": "^3.3.2" }, diff --git a/dashboard/src/app.tsx b/dashboard/src/app.tsx index e9445469d4..a6227455c0 100644 --- a/dashboard/src/app.tsx +++ b/dashboard/src/app.tsx @@ -23,8 +23,7 @@ import { colors } from "./styles/variables" import "flexboxgrid/dist/flexboxgrid.min.css" import "./styles/padding-margin-mixin.scss" import { EventProvider } from "./context/events" -import { ConfigProvider } from "./context/config" -import { StatusProvider } from "./context/status" +import { DataProvider } from "./context/data" const SidebarWrapper = styled.div` border-right: 1px solid ${colors.border}; @@ -36,38 +35,36 @@ const SidebarWrapper = styled.div` const App = () => (
- - - + + +
+ + +
- - - -
-
- - - - -
+ `, "p-2")}> + + + +
- - - +
+
+
) diff --git a/dashboard/src/components/graph/index.tsx b/dashboard/src/components/graph/index.tsx index 247a3a9684..a4c7247c6a 100644 --- a/dashboard/src/components/graph/index.tsx +++ b/dashboard/src/components/graph/index.tsx @@ -224,8 +224,8 @@ class Chart extends Component { } componentDidUpdate(_prevProps: Props) { - const message = this.props.message - if (message.type === "event") { + const { message } = this.props + if (message && message.type === "event") { this.updateNodeClass(message) } } diff --git a/dashboard/src/components/load-wrapper.tsx b/dashboard/src/components/load-wrapper.tsx new file mode 100644 index 0000000000..0caa591a25 --- /dev/null +++ b/dashboard/src/components/load-wrapper.tsx @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import React from "react" + +import Spinner from "./spinner" + +interface LoadContainerProps { + error: Error + loading: boolean + ErrorComponent: React.SFC | React.ComponentClass + LoadComponent?: React.SFC | React.ComponentClass +} + +const LoadWrapper: React.SFC = props => { + const { error, children, loading, ErrorComponent, LoadComponent } = props + if (error) { + return + } else if (loading) { + return LoadComponent === undefined + ? + : LoadComponent === null ? null : + } else { + return
{children}
+ } +} + +export default LoadWrapper diff --git a/dashboard/src/components/overview.tsx b/dashboard/src/components/overview.tsx index a06045a91e..f33824968f 100644 --- a/dashboard/src/components/overview.tsx +++ b/dashboard/src/components/overview.tsx @@ -15,18 +15,11 @@ import Table from "./table" import { ExternalLink } from "./links" import { - FetchStatusResponse, ServiceStatus, - FetchConfigResponse, Module, ServiceIngress, } from "../api/types" -interface Props { - status: FetchStatusResponse - config: FetchConfigResponse -} - export function getIngressUrl(ingress: ServiceIngress) { return normalizeUrl(format({ protocol: ingress.protocol, @@ -36,15 +29,6 @@ export function getIngressUrl(ingress: ServiceIngress) { })) } -const Overview: React.SFC = ({ config, status }) => { - return ( -
- - -
- ) -} - interface ServicesProps { modules: Module[] services: { [name: string]: ServiceStatus } @@ -54,7 +38,7 @@ interface ModulesProps { modules: Module[] } -const Modules: React.SFC = ({ modules }) => { +export const Modules: React.SFC = ({ modules }) => { const rowHeaders = ["Name", "Type", "Services"] const rows = modules.map(module => [ module.name, @@ -70,7 +54,7 @@ const Modules: React.SFC = ({ modules }) => { ) } -const Services: React.SFC = ({ modules, services }) => { +export const Services: React.SFC = ({ modules, services }) => { const rowHeaders = ["Name", "Status", "Module", "Ingresses"] const rows = Object.keys(services).map(service => [ service, @@ -107,5 +91,3 @@ const Ingresses: React.SFC = ({ ingresses }) => { ) } - -export default Overview diff --git a/dashboard/src/containers/fetch-container.tsx b/dashboard/src/containers/fetch-container.tsx deleted file mode 100644 index a59a70a2f3..0000000000 --- a/dashboard/src/containers/fetch-container.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2018 Garden Technologies, Inc. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import React, { Component } from "react" - -import Spinner from "../components/spinner" - -interface Props { - children: (data: { data: T }) => JSX.Element - fetchFn: (...any) => Promise - ErrorComponent: React.SFC | React.ComponentClass - skipSpinner?: boolean -} - -interface State { - error: any - isLoaded: boolean - result: T -} - -class FetchContainer extends Component, State> { - - // TODO This is not type safe - static defaultProps = { - skipSpinner: false, - } - - constructor(props) { - super(props) - this.state = { - error: null, - isLoaded: false, - result: null, - } - } - - componentDidMount() { - // TODO Fetch function parameter? Currently we can just wrap the function. - this.props.fetchFn() - .then( - result => { - this.setState({ - isLoaded: true, - result, - }) - }, - // Note: it's important to handle errors here - // instead of a catch() block so that we don't swallow - // exceptions from actual bugs in components. - error => { - this.setState({ - isLoaded: true, - error, - }) - }, - ) - } - - render() { - const { error, isLoaded, result } = this.state - const { children, ErrorComponent, skipSpinner } = this.props - if (error) { - return - } else if (!isLoaded) { - return skipSpinner ? "" : - } else { - return children({ data: result }) - } - } -} - -export default FetchContainer diff --git a/dashboard/src/containers/graph.tsx b/dashboard/src/containers/graph.tsx index a3c89c9426..7d10aede11 100644 --- a/dashboard/src/containers/graph.tsx +++ b/dashboard/src/containers/graph.tsx @@ -6,31 +6,30 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from "react" - -import { ConfigConsumer } from "../context/config" -import FetchContainer from "./fetch-container" - -import { fetchGraph } from "../api" -// tslint:disable-next-line:no-unused (https://github.com/palantir/tslint/issues/4022) -import { FetchGraphResponse } from "../api/types" +import React, { useContext, useEffect } from "react" import Graph from "../components/graph" import PageError from "../components/page-error" -import { EventConsumer } from "../context/events" +import { EventContext } from "../context/events" +import LoadWrapper from "../components/load-wrapper" +import { DataContext } from "../context/data" + +export default () => { + const { + actions: { loadGraph, loadConfig }, + store: { config, graph }, + } = useContext(DataContext) + const { message } = useContext(EventContext) + + useEffect(loadConfig, []) + useEffect(loadGraph, []) + + const isLoading = !config.data || !graph.data || config.loading || graph.loading + const error = config.error || graph.error -export default () => ( - ErrorComponent={PageError} fetchFn={fetchGraph}> - {({ data: graph }) => ( - - {({ config }) => ( - - {({ message }) => ( - - )} - - )} - - )} - -) + return ( + + + + ) +} diff --git a/dashboard/src/containers/logs.tsx b/dashboard/src/containers/logs.tsx index cdce2e34fa..ab438f32bc 100644 --- a/dashboard/src/containers/logs.tsx +++ b/dashboard/src/containers/logs.tsx @@ -6,26 +6,28 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from "react" +import React, { useContext, useEffect } from "react" -import { ConfigConsumer } from "../context/config" -import FetchContainer from "./fetch-container" +import PageError from "../components/page-error" +import Logs from "../components/logs" +import LoadWrapper from "../components/load-wrapper" +import { DataContext } from "../context/data" -import { fetchLogs } from "../api" -// tslint:disable-next-line:no-unused (https://github.com/palantir/tslint/issues/4022) -import { FetchLogResponse } from "../api/types" +export default () => { + const { + actions: { loadLogs, loadConfig }, + store: { config, logs }, + } = useContext(DataContext) -import Logs from "../components/logs" -import PageError from "../components/page-error" + useEffect(loadConfig, []) + useEffect(loadLogs, []) + + const isLoading = !config.data || !logs.data || config.loading || logs.loading + const error = config.error || logs.error -export default () => ( - ErrorComponent={PageError} fetchFn={fetchLogs}> - {({ data: logs }) => ( - - {({ config }) => ( - - )} - - )} - -) + return ( + + + + ) +} diff --git a/dashboard/src/containers/overview.tsx b/dashboard/src/containers/overview.tsx index a00d42ff58..24ab41436a 100644 --- a/dashboard/src/containers/overview.tsx +++ b/dashboard/src/containers/overview.tsx @@ -6,18 +6,55 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import React from "react" - -import { ConfigConsumer } from "../context/config" -import Overview from "../components/overview" -import { StatusConsumer } from "../context/status" - -export default () => ( - - {({ status }) => ( - - {({ config }) => } - - )} - +import cls from "classnames" +import { css } from "emotion/macro" +import React, { useContext, useEffect } from "react" + +import PageError from "../components/page-error" +import { Modules, Services } from "../components/overview" +import { DataContext } from "../context/data" +import LoadWrapper from "../components/load-wrapper" + +const LoadingServices = () => ( +
+

Loading services...

+
) + +// Note: We render the overview page components individually so we that we don't +// have to wait for both API calls to return. +export default () => { + const { + actions: { loadConfig, loadStatus }, + store: { config, status }, + } = useContext(DataContext) + + useEffect(loadConfig, []) + useEffect(loadStatus, []) + + const isLoadingModules = !config.data || config.loading + const isLoadingServices = !status.data || status.loading + + // Only show when load component for Modules is no longer visible + const ServiceLoadMsg = isLoadingModules ? null : LoadingServices + + return ( +
+ + + + + + +
+ ) +} diff --git a/dashboard/src/containers/sidebar.tsx b/dashboard/src/containers/sidebar.tsx index 39cfb342f0..d420174e26 100644 --- a/dashboard/src/containers/sidebar.tsx +++ b/dashboard/src/containers/sidebar.tsx @@ -6,12 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import cls from "classnames" +import { css } from "emotion/macro" import { kebabCase, flatten, entries } from "lodash" -import React from "react" +import React, { useContext, useEffect } from "react" import Sidebar from "../components/sidebar" import { DashboardPage } from "../api/types" -import { StatusConsumer } from "../context/status" +import { DataContext } from "../context/data" export interface Page extends DashboardPage { path: string @@ -26,8 +28,8 @@ const builtinPages: Page[] = [ url: "", }, { - title: "Service Graph", - description: "Service Graph", + title: "Task Graph", + description: "Task Graph", path: "/graph", newWindow: false, url: "", @@ -41,18 +43,32 @@ const builtinPages: Page[] = [ }, ] -export default () => ( - - {({ status }) => { - const pages: Page[] = flatten(entries(status.providers).map(([providerName, providerStatus]) => { - return providerStatus.dashboardPages.map(p => ({ - ...p, - path: `/provider/${providerName}/${kebabCase(p.title)}`, - description: p.description + ` (from provider ${providerName})`, - })) - })) - - return - }} - -) +export default () => { + const { + actions: { loadStatus }, + store: { status }, + } = useContext(DataContext) + + useEffect(loadStatus, []) + + const isLoading = !status.data || status.loading + if (isLoading) { + return ( +
+

Loading sidebar...

+
+ ) + } + + const pages: Page[] = flatten(entries(status.data.providers).map(([providerName, providerStatus]) => { + return providerStatus.dashboardPages.map(p => ({ + ...p, + path: `/provider/${providerName}/${kebabCase(p.title)}`, + description: p.description + ` (from provider ${providerName})`, + })) + })) + + return +} diff --git a/dashboard/src/context/config.tsx b/dashboard/src/context/config.tsx deleted file mode 100644 index d1161eb8d1..0000000000 --- a/dashboard/src/context/config.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2018 Garden Technologies, Inc. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import React from "react" - -import { fetchConfig } from "../api" -import { FetchConfigResponse } from "../api/types" -import FetchContainer from "../containers/fetch-container" - -type Context = { config: FetchConfigResponse } -const ConfigContext = React.createContext(null) - -const ConfigConsumer = ConfigContext.Consumer - -const Error = () =>

Error loading project configuration. Please try refreshing the page.

- -const ConfigProvider = ({ children }) => ( - ErrorComponent={Error} skipSpinner fetchFn={fetchConfig}> - {({ data: config }) => ( - - {children} - - )} - -) - -export { ConfigProvider, ConfigConsumer } diff --git a/dashboard/src/context/data.tsx b/dashboard/src/context/data.tsx new file mode 100644 index 0000000000..31d024ce73 --- /dev/null +++ b/dashboard/src/context/data.tsx @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { useState, EffectCallback } from "react" +import React from "react" + +import { fetchConfig, fetchLogs, fetchStatus, fetchGraph } from "../api" +import { + FetchConfigResponse, + FetchStatusResponse, + FetchGraphResponse, + FetchLogResponse, +} from "../api/types" + +interface StoreSlice { + error: Error | null + loading: boolean +} + +interface Config extends StoreSlice { data: FetchConfigResponse | null } +interface Status extends StoreSlice { data: FetchStatusResponse | null } +interface Graph extends StoreSlice { data: FetchGraphResponse | null } +interface Logs extends StoreSlice { data: FetchLogResponse | null } + +// This is the global data store +interface Store { + config: Config + status: Status + graph: Graph + logs: Logs, +} + +interface Actions { + loadLogs: EffectCallback + loadConfig: EffectCallback + loadStatus: EffectCallback + loadGraph: EffectCallback +} + +type KeyActionPair = + ["config", () => Promise] | + ["logs", () => Promise] | + ["status", () => Promise] | + ["graph", () => Promise] + +type Context = { + store: Store, + actions: Actions, +} + +type SliceName = keyof Store +const sliceNames: SliceName[] = ["config", "status", "graph", "logs"] + +// TODO Fix type cast +const initialState: Store = sliceNames.reduce((acc, key) => { + const state = { data: null, loading: false, error: null } + acc[key] = state + return acc +}, {}) as Store + +export const DataContext = React.createContext(null) + +// Updates slices of the store based on the slice key +function updateSlice(prevState: Store, key: SliceName, sliceState: Object): Store { + const prevSliceState = prevState[key] + return { + ...prevState, + [key]: { + ...prevSliceState, + ...sliceState, + }, + } +} + +// Creates the actions needed for fetching data from the API and updates the store state as the actions are called. +function useApi() { + const [store, setData] = useState(initialState) + + const fetch = async ([key, fetchFn]: KeyActionPair) => { + setData(prevState => updateSlice(prevState, key, { loading: true })) + + try { + const res = await fetchFn() + setData(prevState => updateSlice(prevState, key, { data: res, error: null })) + } catch (error) { + setData(prevState => updateSlice(prevState, key, { error })) + } + + setData(prevState => updateSlice(prevState, key, { loading: false })) + } + + const fetchOrReadFromStore = (keyActionPair: KeyActionPair, force: boolean) => { + const key = keyActionPair[0] + const { data, loading } = store[key] + if (!force && (data || loading)) { + return + } + fetch(keyActionPair).catch(error => setData(prevState => updateSlice(prevState, key, { error }))) + } + + const loadLogs = (force: boolean = false) => fetchOrReadFromStore(["logs", fetchLogs], force) + const loadConfig = (force: boolean = false) => fetchOrReadFromStore(["config", fetchConfig], force) + const loadGraph = (force: boolean = false) => fetchOrReadFromStore(["graph", fetchGraph], force) + const loadStatus = (force: boolean = false) => fetchOrReadFromStore(["status", fetchStatus], force) + + return { + store, + actions: { + loadConfig, + loadLogs, + loadGraph, + loadStatus, + }, + } +} + +/** + * This component manages the "rest" API data state (not the websockets) for the entire application. + * We use the new React Hooks API to pass store data and actions down the component tree. + */ +export const DataProvider: React.SFC = ({ children }) => { + const storeAndActions = useApi() + + return ( + + {children} + + ) +} diff --git a/dashboard/src/context/events.tsx b/dashboard/src/context/events.tsx index f81ce11126..2bf57e9cfe 100644 --- a/dashboard/src/context/events.tsx +++ b/dashboard/src/context/events.tsx @@ -8,22 +8,60 @@ import React from "react" +import { useEffect, useState } from "react" +import getApiUrl from "../api/get-api-url" import { WsMessage } from "../api/types" -import WsContainer from "../containers/ws-container" type Context = { message?: WsMessage } -const EventContext = React.createContext(null) -const EventConsumer = EventContext.Consumer +export const EventContext = React.createContext(null) -const EventProvider = ({ children }) => ( - - {({ message }) => ( - - {children} - - )} - -) +interface WsOutput { + message: WsMessage | null +} +function useWs(): WsOutput { + const [data, setData] = useState(null) + useEffect(() => { + const url = getApiUrl() + const ws = new WebSocket(`ws://${url}/ws`) -export { EventProvider, EventConsumer } + ws.onopen = event => { + console.log("ws open", event) + } + ws.onclose = event => { + // TODO + console.log("ws close", event) + } + ws.onmessage = msg => { + const message = JSON.parse(msg.data) as WsMessage + + // TOOD + if (message.type === "error") { + console.error(message) + } + + if (message.type === "event") { + console.log(message) + setData({ message }) + } + } + return function cleanUp() { + console.log("ws cleanup") + ws.close() + } + }, []) + + const wsMessage = data ? data.message : null + + return { message: wsMessage } +} + +export const EventProvider: React.SFC = ({ children }) => { + const { message } = useWs() + + return ( + + {children} + + ) +} diff --git a/dashboard/src/context/status.tsx b/dashboard/src/context/status.tsx deleted file mode 100644 index fd2b984993..0000000000 --- a/dashboard/src/context/status.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2018 Garden Technologies, Inc. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import React from "react" - -import { fetchStatus } from "../api" -import { FetchStatusResponse } from "../api/types" -import FetchContainer from "../containers/fetch-container" - -type Context = { status: FetchStatusResponse } -const StatusContext = React.createContext(null) - -const StatusConsumer = StatusContext.Consumer - -const Error = () =>

Error retrieving status

- -const StatusProvider = ({ children }) => ( - ErrorComponent={Error} skipSpinner fetchFn={fetchStatus}> - {({ data: status }) => ( - - {children} - - )} - -) - -export { StatusProvider, StatusConsumer } diff --git a/dashboard/src/util/create-provider.tsx b/dashboard/src/util/create-provider.tsx deleted file mode 100644 index bdda5fcf83..0000000000 --- a/dashboard/src/util/create-provider.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2018 Garden Technologies, Inc. - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import React from "react" -import FetchContainer from "../containers/fetch-container" - -// TODO Add types to return components -export function makeProvider(fetchFn, ErrorComponent: React.SFC | React.ComponentClass) { - - type Ctx = { data: T } - const Ctx = React.createContext(null) - - const Provider = ({ children }) => ( - ErrorComponent={ErrorComponent} fetchFn={fetchFn}> - {({ data }) => ( - - {children} - - )} - - ) - - return { Provider, Consumer: Ctx.Consumer } -}