mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 16:47:24 +08:00
[CORL-1016] Admin Dashboard (#2942)
* count things in redis * add rest routes for daily comment totals * add route for new commenters * retrieve hourly totals for new commenters and comments * add rate limiting * add routes and components for dashboard * add top stories component * add activity charts * clean up rest responses * style dashboard components * make dashboard site-aware * change new commenters to new signups * generate new signups by day chart * update status pie chart * update status pie chart * allow site selection in dashboard * cache daily signups * count user bans in redis * clean up route and method names * clean up comment statuses chart and change from pie to bar * fix package lock * make daily counts time zone aware * count rejected comments * clean up today counts * count comments from site * store hourly keys in utc * show daily user signups * move siteID from params to query params * add average line for daily comments chart * make average comments count hourly * show percent values for rejected comments * style dashboard * simplify dashboard rest routes * style today totals and top stories * style signup and comment activity chart * add site selector to nav * feat: redis/mongo refactor * fix: some small tweaks - Added comments to magic numbers - Added errors for missing input - Consolidated promise resolutions * Revert "add site selector to nav" This reverts commit 1c2b1dee34fb2742b04932079fd45f7f3de98418. * show first site dashboard by default * style dashboard site switcher * updte snapshot * add dashboard 118n keys * udpate comment activiyt chart with legend and tooltip * implement refresh button and loading states * change colours of charts for today * adjust today values spacing * remove unused package * don't pass redis into oauth strat * remove unused package * Revert "remove unused package" This reverts commit 5b7c83a072604810ce7097865655e9ef8114d9e0. * fix merge conflicts * Fix icons bug on smaller screens * resolve merge conflict Co-authored-by: Wyatt Johnson <me@wyattjoh.ca>
This commit is contained in:
Vendored
+1
-1
@@ -54,5 +54,5 @@
|
|||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"package-lock.json": true
|
"package-lock.json": true
|
||||||
},
|
},
|
||||||
"debug.node.autoAttach": "off",
|
"debug.node.autoAttach": "off"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+282
-93
@@ -7324,6 +7324,19 @@
|
|||||||
"postcss": "7.x.x"
|
"postcss": "7.x.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/d3-path": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA=="
|
||||||
|
},
|
||||||
|
"@types/d3-shape": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-LtD8EaNYCaBRzHzaAiIPrfcL3DdIysc81dkGlQvv7WQP3+YXV7b0JJTtR1U3bzeRieS603KF4wUo+ZkJVenh8w==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/debug": {
|
"@types/debug": {
|
||||||
"version": "0.0.30",
|
"version": "0.0.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz",
|
||||||
@@ -8007,8 +8020,7 @@
|
|||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.5.8",
|
"version": "15.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz",
|
||||||
"integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw==",
|
"integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/q": {
|
"@types/q": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
@@ -8040,7 +8052,6 @@
|
|||||||
"version": "16.9.31",
|
"version": "16.9.31",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.31.tgz",
|
||||||
"integrity": "sha512-NpYJpNMWScFXtx3A2BJMeew2G3+9SEslVWMdxNJ6DLvxIuxWjY1bizK9q5Y1ujhln31vtjmhjOAYDr9Xx3k9FQ==",
|
"integrity": "sha512-NpYJpNMWScFXtx3A2BJMeew2G3+9SEslVWMdxNJ6DLvxIuxWjY1bizK9q5Y1ujhln31vtjmhjOAYDr9Xx3k9FQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^2.2.0"
|
"csstype": "^2.2.0"
|
||||||
@@ -8129,6 +8140,21 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/recharts": {
|
||||||
|
"version": "1.8.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.11.tgz",
|
||||||
|
"integrity": "sha512-1E1+3uKo7EyNk3LZ6J2cqfayIdQEUt1YOq2Y/9eRSXUzasf100ncF4f0iARnaXnED6DyFgTPRomwtChcwLYvhA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/d3-shape": "*",
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/recharts-scale": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/recharts-scale": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/recharts-scale/-/recharts-scale-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-HR/PrCcxYb2YHviTqH7CMdL1TUhUZLTUKzfrkMhxm1HTa5mg/QtP8XMiuSPz6dZ6wecazAOu8aYZ5DqkNlgHHQ=="
|
||||||
|
},
|
||||||
"@types/recompose": {
|
"@types/recompose": {
|
||||||
"version": "0.30.7",
|
"version": "0.30.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/recompose/-/recompose-0.30.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/recompose/-/recompose-0.30.7.tgz",
|
||||||
@@ -13023,8 +13049,7 @@
|
|||||||
"classnames": {
|
"classnames": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||||
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==",
|
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"clean-css": {
|
"clean-css": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
@@ -15621,6 +15646,73 @@
|
|||||||
"es5-ext": "^0.10.9"
|
"es5-ext": "^0.10.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"d3-array": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
|
||||||
|
},
|
||||||
|
"d3-collection": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
|
||||||
|
},
|
||||||
|
"d3-color": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
|
||||||
|
},
|
||||||
|
"d3-format": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw=="
|
||||||
|
},
|
||||||
|
"d3-interpolate": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
|
||||||
|
"requires": {
|
||||||
|
"d3-color": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-path": {
|
||||||
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
|
||||||
|
},
|
||||||
|
"d3-scale": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
|
||||||
|
"requires": {
|
||||||
|
"d3-array": "^1.2.0",
|
||||||
|
"d3-collection": "1",
|
||||||
|
"d3-format": "1",
|
||||||
|
"d3-interpolate": "1",
|
||||||
|
"d3-time": "1",
|
||||||
|
"d3-time-format": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-shape": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
|
||||||
|
"requires": {
|
||||||
|
"d3-path": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"d3-time": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
|
||||||
|
},
|
||||||
|
"d3-time-format": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==",
|
||||||
|
"requires": {
|
||||||
|
"d3-time": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"damerau-levenshtein": {
|
"damerau-levenshtein": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz",
|
||||||
@@ -15927,6 +16019,11 @@
|
|||||||
"integrity": "sha1-rzJJRl4TOYjDB1D3fqr0RQXKpeM=",
|
"integrity": "sha1-rzJJRl4TOYjDB1D3fqr0RQXKpeM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"decimal.js-light": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg=="
|
||||||
|
},
|
||||||
"decode-uri-component": {
|
"decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||||
@@ -17539,7 +17636,6 @@
|
|||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
||||||
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.1.2"
|
"@babel/runtime": "^7.1.2"
|
||||||
}
|
}
|
||||||
@@ -21389,28 +21485,28 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"abbrev": {
|
"abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||||
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
|
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"are-we-there-yet": {
|
"are-we-there-yet": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
|
||||||
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
|
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21421,14 +21517,14 @@
|
|||||||
},
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21439,42 +21535,42 @@
|
|||||||
},
|
},
|
||||||
"chownr": {
|
"chownr": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
|
||||||
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
|
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21484,28 +21580,28 @@
|
|||||||
},
|
},
|
||||||
"deep-extend": {
|
"deep-extend": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"delegates": {
|
"delegates": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"detect-libc": {
|
"detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
|
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"fs-minipass": {
|
"fs-minipass": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
|
||||||
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
|
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21515,14 +21611,14 @@
|
|||||||
},
|
},
|
||||||
"fs.realpath": {
|
"fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"gauge": {
|
"gauge": {
|
||||||
"version": "2.7.4",
|
"version": "2.7.4",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||||
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
|
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21539,7 +21635,7 @@
|
|||||||
},
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||||
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21554,14 +21650,14 @@
|
|||||||
},
|
},
|
||||||
"has-unicode": {
|
"has-unicode": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
|
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"iconv-lite": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21571,7 +21667,7 @@
|
|||||||
},
|
},
|
||||||
"ignore-walk": {
|
"ignore-walk": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
|
||||||
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
|
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21581,7 +21677,7 @@
|
|||||||
},
|
},
|
||||||
"inflight": {
|
"inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21592,21 +21688,21 @@
|
|||||||
},
|
},
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"is-fullwidth-code-point": {
|
"is-fullwidth-code-point": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21616,14 +21712,14 @@
|
|||||||
},
|
},
|
||||||
"isarray": {
|
"isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21633,14 +21729,14 @@
|
|||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz",
|
||||||
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
|
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21651,7 +21747,7 @@
|
|||||||
},
|
},
|
||||||
"minizlib": {
|
"minizlib": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
|
||||||
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
|
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21661,7 +21757,7 @@
|
|||||||
},
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21671,7 +21767,7 @@
|
|||||||
},
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
@@ -21685,7 +21781,7 @@
|
|||||||
},
|
},
|
||||||
"needle": {
|
"needle": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz",
|
||||||
"integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
|
"integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21697,7 +21793,7 @@
|
|||||||
},
|
},
|
||||||
"node-pre-gyp": {
|
"node-pre-gyp": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz",
|
||||||
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
|
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21716,7 +21812,7 @@
|
|||||||
},
|
},
|
||||||
"nopt": {
|
"nopt": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
|
||||||
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
|
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21727,14 +21823,14 @@
|
|||||||
},
|
},
|
||||||
"npm-bundled": {
|
"npm-bundled": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
|
||||||
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
|
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"npm-packlist": {
|
"npm-packlist": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz",
|
||||||
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
|
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21745,7 +21841,7 @@
|
|||||||
},
|
},
|
||||||
"npmlog": {
|
"npmlog": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21758,21 +21854,21 @@
|
|||||||
},
|
},
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"once": {
|
"once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21782,21 +21878,21 @@
|
|||||||
},
|
},
|
||||||
"os-homedir": {
|
"os-homedir": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
|
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"os-tmpdir": {
|
"os-tmpdir": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
|
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"osenv": {
|
"osenv": {
|
||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
|
||||||
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
|
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21807,21 +21903,21 @@
|
|||||||
},
|
},
|
||||||
"path-is-absolute": {
|
"path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"process-nextick-args": {
|
"process-nextick-args": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
|
||||||
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
|
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"rc": {
|
"rc": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21834,7 +21930,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
@@ -21843,7 +21939,7 @@
|
|||||||
},
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21859,7 +21955,7 @@
|
|||||||
},
|
},
|
||||||
"rimraf": {
|
"rimraf": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||||
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
|
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21869,49 +21965,49 @@
|
|||||||
},
|
},
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"sax": {
|
"sax": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
|
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.7.0",
|
"version": "5.7.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
|
||||||
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
|
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"set-blocking": {
|
"set-blocking": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
|
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
|
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"string-width": {
|
"string-width": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21923,7 +22019,7 @@
|
|||||||
},
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21933,7 +22029,7 @@
|
|||||||
},
|
},
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||||
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21943,14 +22039,14 @@
|
|||||||
},
|
},
|
||||||
"strip-json-comments": {
|
"strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"tar": {
|
"tar": {
|
||||||
"version": "4.4.8",
|
"version": "4.4.8",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz",
|
||||||
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
|
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21966,14 +22062,14 @@
|
|||||||
},
|
},
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"wide-align": {
|
"wide-align": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
|
||||||
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
|
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -21983,14 +22079,14 @@
|
|||||||
},
|
},
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": false,
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
|
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
@@ -33108,8 +33204,7 @@
|
|||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
||||||
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
|
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"js-yaml": {
|
"js-yaml": {
|
||||||
"version": "3.13.1",
|
"version": "3.13.1",
|
||||||
@@ -34018,7 +34113,7 @@
|
|||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
|
||||||
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -34314,8 +34409,7 @@
|
|||||||
"lodash.debounce": {
|
"lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
|
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.deburr": {
|
"lodash.deburr": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
@@ -34494,9 +34588,7 @@
|
|||||||
"lodash.throttle": {
|
"lodash.throttle": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=",
|
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"lodash.toarray": {
|
"lodash.toarray": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
@@ -34635,7 +34727,6 @@
|
|||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
|
||||||
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
|
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"js-tokens": "^3.0.0"
|
"js-tokens": "^3.0.0"
|
||||||
}
|
}
|
||||||
@@ -34885,6 +34976,11 @@
|
|||||||
"integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=",
|
"integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"math-expression-evaluator": {
|
||||||
|
"version": "1.2.22",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz",
|
||||||
|
"integrity": "sha512-L0j0tFVZBQQLeEjmWOvDLoRciIY8gQGWahvkztXUal8jH8R5Rlqo9GCvgqvXcy9LQhEWdQCVvzqAbxgYNt4blQ=="
|
||||||
|
},
|
||||||
"mathjs": {
|
"mathjs": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-2.7.0.tgz",
|
||||||
@@ -42971,7 +43067,6 @@
|
|||||||
"version": "15.6.2",
|
"version": "15.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
|
||||||
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.3.1",
|
"loose-envify": "^1.3.1",
|
||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
@@ -43178,7 +43273,6 @@
|
|||||||
"version": "3.4.1",
|
"version": "3.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"performance-now": "^2.1.0"
|
"performance-now": "^2.1.0"
|
||||||
}
|
}
|
||||||
@@ -44000,8 +44094,7 @@
|
|||||||
"react-fast-compare": {
|
"react-fast-compare": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
|
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"react-feather": {
|
"react-feather": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
@@ -44063,7 +44156,6 @@
|
|||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz",
|
||||||
"integrity": "sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==",
|
"integrity": "sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"prop-types": "^15.5.4",
|
"prop-types": "^15.5.4",
|
||||||
@@ -44165,8 +44257,7 @@
|
|||||||
"react-lifecycles-compat": {
|
"react-lifecycles-compat": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"react-live": {
|
"react-live": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
@@ -44395,7 +44486,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz",
|
||||||
"integrity": "sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==",
|
"integrity": "sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"shallowequal": "^1.0.1"
|
"shallowequal": "^1.0.1"
|
||||||
}
|
}
|
||||||
@@ -44406,6 +44496,38 @@
|
|||||||
"integrity": "sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA==",
|
"integrity": "sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"react-smooth": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-eW057HT0lFgCKh8ilr0y2JaH2YbNcuEdFpxyg7Gf/qDKk9hqGMyXryZJ8iMGJEuKH0+wxS0ccSsBBB3W8yCn8w==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "~4.17.4",
|
||||||
|
"prop-types": "^15.6.0",
|
||||||
|
"raf": "^3.4.0",
|
||||||
|
"react-transition-group": "^2.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"requires": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"react-transition-group": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
|
||||||
|
"requires": {
|
||||||
|
"dom-helpers": "^3.4.0",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react-lifecycles-compat": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-static-container": {
|
"react-static-container": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-static-container/-/react-static-container-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-static-container/-/react-static-container-1.0.2.tgz",
|
||||||
@@ -44585,6 +44707,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"recharts": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-tM9mprJbXVEBxjM7zHsIy6Cc41oO/pVYqyAsOHLxlJrbNBuLs0PHB3iys2M+RqCF0//k8nJtZF6X6swSkWY3tg==",
|
||||||
|
"requires": {
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"core-js": "^2.6.10",
|
||||||
|
"d3-interpolate": "^1.3.0",
|
||||||
|
"d3-scale": "^2.1.0",
|
||||||
|
"d3-shape": "^1.2.0",
|
||||||
|
"lodash": "^4.17.5",
|
||||||
|
"prop-types": "^15.6.0",
|
||||||
|
"react-resize-detector": "^2.3.0",
|
||||||
|
"react-smooth": "^1.0.5",
|
||||||
|
"recharts-scale": "^0.4.2",
|
||||||
|
"reduce-css-calc": "^1.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": {
|
||||||
|
"version": "2.6.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||||
|
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
|
||||||
|
},
|
||||||
|
"react-resize-detector": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ==",
|
||||||
|
"requires": {
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.throttle": "^4.1.1",
|
||||||
|
"prop-types": "^15.6.0",
|
||||||
|
"resize-observer-polyfill": "^1.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recharts-scale": {
|
||||||
|
"version": "0.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.3.tgz",
|
||||||
|
"integrity": "sha512-t8p5sccG9Blm7c1JQK/ak9O8o95WGhNXD7TXg/BW5bYbVlr6eCeRBNpgyigD4p6pSSMehC5nSvBUPj6F68rbFA==",
|
||||||
|
"requires": {
|
||||||
|
"decimal.js-light": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"rechoir": {
|
"rechoir": {
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
|
||||||
@@ -44653,6 +44819,31 @@
|
|||||||
"redis-errors": "^1.0.0"
|
"redis-errors": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"reduce-css-calc": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz",
|
||||||
|
"integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=",
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^0.4.2",
|
||||||
|
"math-expression-evaluator": "^1.2.14",
|
||||||
|
"reduce-function-call": "^1.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
|
||||||
|
"integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reduce-function-call": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==",
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"redux": {
|
"redux": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
|
||||||
@@ -45914,8 +46105,7 @@
|
|||||||
"resize-observer-polyfill": {
|
"resize-observer-polyfill": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
@@ -46446,8 +46636,7 @@
|
|||||||
"shallowequal": {
|
"shallowequal": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
|
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"shebang-command": {
|
"shebang-command": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|||||||
+3
-1
@@ -65,6 +65,7 @@
|
|||||||
"@hapi/joi": "^17.1.1",
|
"@hapi/joi": "^17.1.1",
|
||||||
"@metascraper/helpers": "^5.11.6",
|
"@metascraper/helpers": "^5.11.6",
|
||||||
"@rudderstack/rudder-sdk-node": "0.0.2",
|
"@rudderstack/rudder-sdk-node": "0.0.2",
|
||||||
|
"@types/recharts": "^1.8.9",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
"akismet-api": "^5.0.0",
|
"akismet-api": "^5.0.0",
|
||||||
"apollo-server-express": "^2.11.0",
|
"apollo-server-express": "^2.11.0",
|
||||||
@@ -135,6 +136,8 @@
|
|||||||
"querystringify": "^2.1.1",
|
"querystringify": "^2.1.1",
|
||||||
"source-map-support": "^0.5.16",
|
"source-map-support": "^0.5.16",
|
||||||
"stack-utils": "^2.0.1",
|
"stack-utils": "^2.0.1",
|
||||||
|
"react-helmet": "^5.2.1",
|
||||||
|
"recharts": "^1.8.5",
|
||||||
"striptags": "^3.1.1",
|
"striptags": "^3.1.1",
|
||||||
"tsscmp": "^1.0.6",
|
"tsscmp": "^1.0.6",
|
||||||
"url-regex": "^5.0.0",
|
"url-regex": "^5.0.0",
|
||||||
@@ -327,7 +330,6 @@
|
|||||||
"react-error-overlay": "^6.0.7",
|
"react-error-overlay": "^6.0.7",
|
||||||
"react-final-form": "6.3.0",
|
"react-final-form": "6.3.0",
|
||||||
"react-final-form-arrays": "^3.1.1",
|
"react-final-form-arrays": "^3.1.1",
|
||||||
"react-helmet": "^5.2.1",
|
|
||||||
"react-popper": "^1.3.7",
|
"react-popper": "^1.3.7",
|
||||||
"react-relay": "^9.0.0",
|
"react-relay": "^9.0.0",
|
||||||
"react-relay-network-modern": "^4.6.1",
|
"react-relay-network-modern": "^4.6.1",
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ import Navigation from "./Navigation";
|
|||||||
|
|
||||||
it("renders correctly", () => {
|
it("renders correctly", () => {
|
||||||
const renderer = createRenderer();
|
const renderer = createRenderer();
|
||||||
renderer.render(<Navigation showConfigure />);
|
renderer.render(<Navigation showConfigure showDashboard={true} />);
|
||||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import NavigationLink from "./NavigationLink";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showConfigure: boolean;
|
showConfigure: boolean;
|
||||||
|
showDashboard: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Navigation: FunctionComponent<Props> = (props) => (
|
const Navigation: FunctionComponent<Props> = (props) => (
|
||||||
@@ -25,6 +26,11 @@ const Navigation: FunctionComponent<Props> = (props) => (
|
|||||||
<NavigationLink to="/admin/configure">Configure</NavigationLink>
|
<NavigationLink to="/admin/configure">Configure</NavigationLink>
|
||||||
</Localized>
|
</Localized>
|
||||||
)}
|
)}
|
||||||
|
{props.showDashboard && (
|
||||||
|
<Localized id="navigation-dashboard">
|
||||||
|
<NavigationLink to="/admin/dashboard">Dashboard</NavigationLink>
|
||||||
|
</Localized>
|
||||||
|
)}
|
||||||
</AppBarNavigation>
|
</AppBarNavigation>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ class NavigationContainer extends React.Component<Props> {
|
|||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<Navigation
|
<Navigation
|
||||||
|
showDashboard={
|
||||||
|
!!this.props.viewer && can(this.props.viewer, Ability.VIEW_STATISTICS)
|
||||||
|
}
|
||||||
showConfigure={
|
showConfigure={
|
||||||
!!this.props.viewer &&
|
!!this.props.viewer &&
|
||||||
can(this.props.viewer, Ability.CHANGE_CONFIGURATION)
|
can(this.props.viewer, Ability.CHANGE_CONFIGURATION)
|
||||||
|
|||||||
@@ -38,5 +38,14 @@ exports[`renders correctly 1`] = `
|
|||||||
Configure
|
Configure
|
||||||
</NavigationLink>
|
</NavigationLink>
|
||||||
</Localized>
|
</Localized>
|
||||||
|
<Localized
|
||||||
|
id="navigation-dashboard"
|
||||||
|
>
|
||||||
|
<NavigationLink
|
||||||
|
to="/admin/dashboard"
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</NavigationLink>
|
||||||
|
</Localized>
|
||||||
</withPropsOnChange(Navigation)>
|
</withPropsOnChange(Navigation)>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const permissionMap = {
|
|||||||
CHANGE_STORY_STATUS: [GQLUSER_ROLE.ADMIN, GQLUSER_ROLE.MODERATOR],
|
CHANGE_STORY_STATUS: [GQLUSER_ROLE.ADMIN, GQLUSER_ROLE.MODERATOR],
|
||||||
// Mutation.inviteUsers
|
// Mutation.inviteUsers
|
||||||
INVITE_USERS: [GQLUSER_ROLE.ADMIN],
|
INVITE_USERS: [GQLUSER_ROLE.ADMIN],
|
||||||
|
VIEW_STATISTICS: [GQLUSER_ROLE.ADMIN, GQLUSER_ROLE.MODERATOR],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AbilityType = keyof typeof permissionMap;
|
export type AbilityType = keyof typeof permissionMap;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import { Sites } from "./routes/Configure/sections/Sites";
|
|||||||
import AddSiteRoute from "./routes/Configure/sections/Sites/AddSiteRoute";
|
import AddSiteRoute from "./routes/Configure/sections/Sites/AddSiteRoute";
|
||||||
import SiteRoute from "./routes/Configure/sections/Sites/SiteRoute";
|
import SiteRoute from "./routes/Configure/sections/Sites/SiteRoute";
|
||||||
import WebhookEndpointsLayout from "./routes/Configure/sections/WebhookEndpoints/WebhookEndpointsLayout";
|
import WebhookEndpointsLayout from "./routes/Configure/sections/WebhookEndpoints/WebhookEndpointsLayout";
|
||||||
|
import DashboardRoute from "./routes/Dashboard";
|
||||||
|
import SiteDashboardRoute from "./routes/Dashboard/SiteDashboardRoute";
|
||||||
import ForgotPasswordRoute from "./routes/ForgotPassword";
|
import ForgotPasswordRoute from "./routes/ForgotPassword";
|
||||||
import InviteRoute from "./routes/Invite";
|
import InviteRoute from "./routes/Invite";
|
||||||
import LoginRoute from "./routes/Login";
|
import LoginRoute from "./routes/Login";
|
||||||
@@ -111,6 +113,8 @@ export default makeRouteConfig(
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="stories" {...StoriesRoute.routeConfig} />
|
<Route path="stories" {...StoriesRoute.routeConfig} />
|
||||||
|
<Route path="dashboard" {...DashboardRoute.routeConfig} />
|
||||||
|
<Route path="dashboard/:siteID" {...SiteDashboardRoute.routeConfig} />
|
||||||
<Route path="community" {...CommunityRoute.routeConfig} />
|
<Route path="community" {...CommunityRoute.routeConfig} />
|
||||||
<Route
|
<Route
|
||||||
{...createAuthCheckRoute({
|
{...createAuthCheckRoute({
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.columns {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import { Flex, HorizontalGutter } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import { CommentActivity, SignupActivity, Today, TopStories } from "./sections";
|
||||||
|
|
||||||
|
import styles from "./Dashboard.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
siteID: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Dashboard: FunctionComponent<Props> = (props) => (
|
||||||
|
<HorizontalGutter spacing={4}>
|
||||||
|
<Today siteID={props.siteID} lastUpdated={props.lastUpdated} />
|
||||||
|
<Flex spacing={5}>
|
||||||
|
<HorizontalGutter className={styles.columns} spacing={4}>
|
||||||
|
<CommentActivity
|
||||||
|
siteID={props.siteID}
|
||||||
|
lastUpdated={props.lastUpdated}
|
||||||
|
/>
|
||||||
|
<SignupActivity siteID={props.siteID} lastUpdated={props.lastUpdated} />
|
||||||
|
</HorizontalGutter>
|
||||||
|
<div className={styles.columns}>
|
||||||
|
<TopStories siteID={props.siteID} lastUpdated={props.lastUpdated} />
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</HorizontalGutter>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.root {
|
||||||
|
margin-top: var(--v2-spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--v2-colors-mono-100);
|
||||||
|
margin-top: var(--v2-spacing-1);
|
||||||
|
margin-left: var(--v2-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
font-weight: var(--v2-font-weight-primary-bold);
|
||||||
|
font-size: var(--v2-font-size-6);
|
||||||
|
color: var(--v2-colors-teal-600);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { graphql, RelayPaginationProp } from "react-relay";
|
||||||
|
|
||||||
|
import MainLayout from "coral-admin/components/MainLayout";
|
||||||
|
import { IntersectionProvider } from "coral-framework/lib/intersection";
|
||||||
|
import {
|
||||||
|
useLoadMore,
|
||||||
|
useRefetch,
|
||||||
|
withPaginationContainer,
|
||||||
|
} from "coral-framework/lib/relay";
|
||||||
|
import {
|
||||||
|
BaseButton,
|
||||||
|
Button,
|
||||||
|
ButtonIcon,
|
||||||
|
ClickOutside,
|
||||||
|
Dropdown,
|
||||||
|
Flex,
|
||||||
|
Popover,
|
||||||
|
} from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import { DashboardContainer_query as QueryData } from "coral-admin/__generated__/DashboardContainer_query.graphql";
|
||||||
|
import { DashboardContainerPaginationQueryVariables } from "coral-admin/__generated__/DashboardContainerPaginationQuery.graphql";
|
||||||
|
|
||||||
|
import SiteDashboardTimestamp from "./components/SiteDashboardTimestamp";
|
||||||
|
import Dashboard from "./Dashboard";
|
||||||
|
import DashboardSiteSelector from "./DashboardSiteSelector";
|
||||||
|
|
||||||
|
import styles from "./DashboardContainer.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
query: QueryData | null;
|
||||||
|
relay: RelayPaginationProp;
|
||||||
|
selectedSiteID?: string;
|
||||||
|
}
|
||||||
|
const DashboardContainer: React.FunctionComponent<Props> = (props) => {
|
||||||
|
const sites = props.query
|
||||||
|
? props.query.sites.edges.map((edge) => edge.node)
|
||||||
|
: [];
|
||||||
|
const selectedSite = props.selectedSiteID
|
||||||
|
? sites.find((s) => s.id === props.selectedSiteID)
|
||||||
|
: sites[0];
|
||||||
|
|
||||||
|
if (!selectedSite) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const [lastUpdated, setLastUpdated] = useState<string>(new Date().toString());
|
||||||
|
const [loadMore, isLoadingMore] = useLoadMore(props.relay, 10);
|
||||||
|
const [, isRefetching] = useRefetch<
|
||||||
|
DashboardContainerPaginationQueryVariables
|
||||||
|
>(props.relay);
|
||||||
|
const onRefetch = useCallback(() => {
|
||||||
|
setLastUpdated(new Date().toString());
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<MainLayout className={styles.root}>
|
||||||
|
<Popover
|
||||||
|
id="dashboardSiteSwitcher"
|
||||||
|
placement="bottom-end"
|
||||||
|
description="A dialog of the user menu with related links and actions"
|
||||||
|
body={({ toggleVisibility }) => (
|
||||||
|
<ClickOutside onClickOutside={toggleVisibility}>
|
||||||
|
<Dropdown>
|
||||||
|
<IntersectionProvider>
|
||||||
|
<DashboardSiteSelector
|
||||||
|
loading={!props.query || isRefetching}
|
||||||
|
sites={sites}
|
||||||
|
onLoadMore={loadMore}
|
||||||
|
hasMore={!isRefetching && props.relay.hasMore()}
|
||||||
|
disableLoadMore={isLoadingMore}
|
||||||
|
/>
|
||||||
|
</IntersectionProvider>
|
||||||
|
</Dropdown>
|
||||||
|
</ClickOutside>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ toggleVisibility, ref, visible }) => (
|
||||||
|
<BaseButton onClick={toggleVisibility} ref={ref}>
|
||||||
|
<Flex>
|
||||||
|
<h2 className={styles.header}>{selectedSite.name}</h2>
|
||||||
|
{
|
||||||
|
<ButtonIcon className={styles.icon} size="lg">
|
||||||
|
{visible ? "arrow_drop_up" : "arrow_drop_down"}
|
||||||
|
</ButtonIcon>
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
</BaseButton>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
<Flex alignItems="center" spacing={2}>
|
||||||
|
<SiteDashboardTimestamp />
|
||||||
|
<Button variant="text" onClick={onRefetch} iconLeft>
|
||||||
|
<ButtonIcon>refresh</ButtonIcon>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<Dashboard siteID={selectedSite.id} lastUpdated={lastUpdated} />
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type FragmentVariables = DashboardContainerPaginationQueryVariables;
|
||||||
|
|
||||||
|
const enhanced = withPaginationContainer<
|
||||||
|
Props,
|
||||||
|
DashboardContainerPaginationQueryVariables,
|
||||||
|
FragmentVariables
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
query: graphql`
|
||||||
|
fragment DashboardContainer_query on Query
|
||||||
|
@argumentDefinitions(
|
||||||
|
count: { type: "Int!", defaultValue: 20 }
|
||||||
|
cursor: { type: "Cursor" }
|
||||||
|
) {
|
||||||
|
sites(first: $count, after: $cursor)
|
||||||
|
@connection(key: "SitesConfig_sites") {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
...DashboardSiteContainer_site
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: "forward",
|
||||||
|
getConnectionFromProps(props) {
|
||||||
|
return props.query && props.query.sites;
|
||||||
|
},
|
||||||
|
// This is also the default implementation of `getFragmentVariables` if it isn't provided.
|
||||||
|
getFragmentVariables(prevVars, totalCount) {
|
||||||
|
return {
|
||||||
|
...prevVars,
|
||||||
|
count: totalCount,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getVariables(props, { count, cursor }, fragmentVariables) {
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
cursor,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
query: graphql`
|
||||||
|
# Pagination query to be fetched upon calling 'loadMore'.
|
||||||
|
# Notice that we re-use our fragment, and the shape of this query matches our fragment spec.
|
||||||
|
query DashboardContainerPaginationQuery($count: Int!, $cursor: Cursor) {
|
||||||
|
...DashboardContainer_query @arguments(count: $count, cursor: $cursor)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
)(DashboardContainer);
|
||||||
|
export default enhanced;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { graphql } from "react-relay";
|
||||||
|
|
||||||
|
import { withRouteConfig } from "coral-framework/lib/router";
|
||||||
|
|
||||||
|
import { DashboardRouteQueryResponse } from "coral-admin/__generated__/DashboardRouteQuery.graphql";
|
||||||
|
|
||||||
|
import DashboardSiteSelectorContainer from "./DashboardContainer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: DashboardRouteQueryResponse | null;
|
||||||
|
}
|
||||||
|
const DashboardRoute: React.FunctionComponent<Props> = ({ data }) => {
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DashboardSiteSelectorContainer query={data} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhanced = withRouteConfig<Props>({
|
||||||
|
query: graphql`
|
||||||
|
query DashboardRouteQuery {
|
||||||
|
...DashboardContainer_query
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})(DashboardRoute);
|
||||||
|
|
||||||
|
export default enhanced;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.button {
|
||||||
|
color: var(--v2-palette-primary-main);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
import { graphql } from "react-relay";
|
||||||
|
|
||||||
|
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||||
|
import { DropdownButton } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import { DashboardSiteContainer_site } from "coral-admin/__generated__/DashboardSiteContainer_site.graphql";
|
||||||
|
|
||||||
|
import styles from "./DashboardSiteContainer.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
site: DashboardSiteContainer_site;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardSiteContainer: FunctionComponent<Props> = ({ site }) => {
|
||||||
|
return (
|
||||||
|
<DropdownButton
|
||||||
|
className={styles.button}
|
||||||
|
to={`/admin/dashboard/${site.id}`}
|
||||||
|
>
|
||||||
|
{site.name}
|
||||||
|
</DropdownButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhanced = withFragmentContainer<Props>({
|
||||||
|
site: graphql`
|
||||||
|
fragment DashboardSiteContainer_site on Site {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})(DashboardSiteContainer);
|
||||||
|
|
||||||
|
export default enhanced;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import AutoLoadMore from "coral-admin/components/AutoLoadMore";
|
||||||
|
import { PropTypesOf } from "coral-framework/types";
|
||||||
|
import { Flex, Spinner } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import DashboardSiteContainer from "./DashboardSiteContainer";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
sites: Array<
|
||||||
|
{ id: string } & PropTypesOf<typeof DashboardSiteContainer>["site"]
|
||||||
|
>;
|
||||||
|
onLoadMore: () => void;
|
||||||
|
hasMore: boolean;
|
||||||
|
disableLoadMore: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SitesTable: FunctionComponent<Props> = (props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.sites.map((site) => (
|
||||||
|
<DashboardSiteContainer site={site} key={site.id} />
|
||||||
|
))}
|
||||||
|
{props.loading && (
|
||||||
|
<Flex justifyContent="center">
|
||||||
|
<Spinner />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{props.hasMore && (
|
||||||
|
<Flex justifyContent="center">
|
||||||
|
<AutoLoadMore
|
||||||
|
disableLoadMore={props.disableLoadMore}
|
||||||
|
onLoadMore={props.onLoadMore}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SitesTable;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Match, Router } from "found";
|
||||||
|
import React from "react";
|
||||||
|
import { graphql } from "react-relay";
|
||||||
|
|
||||||
|
import { withRouteConfig } from "coral-framework/lib/router";
|
||||||
|
|
||||||
|
import { SiteDashboardRouteQueryResponse } from "coral-admin/__generated__/SiteDashboardRouteQuery.graphql";
|
||||||
|
|
||||||
|
import DashboardSiteSelectorContainer from "./DashboardContainer";
|
||||||
|
|
||||||
|
interface RouteParams {
|
||||||
|
siteID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: SiteDashboardRouteQueryResponse | null;
|
||||||
|
router: Router;
|
||||||
|
match: Match & { params: RouteParams };
|
||||||
|
}
|
||||||
|
const SiteDashboardRoute: React.FunctionComponent<Props> = (props) => {
|
||||||
|
const { data } = props;
|
||||||
|
if (data && data.site) {
|
||||||
|
return (
|
||||||
|
<DashboardSiteSelectorContainer
|
||||||
|
query={data}
|
||||||
|
selectedSiteID={props.match.params.siteID}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const enhanced = withRouteConfig<Props>({
|
||||||
|
query: graphql`
|
||||||
|
query SiteDashboardRouteQuery($siteID: ID!) {
|
||||||
|
...DashboardContainer_query
|
||||||
|
site(id: $siteID) {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})(SiteDashboardRoute);
|
||||||
|
|
||||||
|
export default enhanced;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.root {
|
||||||
|
border: 1px solid var(--v2-colors-grey-300);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: var(--v2-spacing-3);
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import cn from "classnames";
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import styles from "./DashboardBox.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardBox: FunctionComponent<Props> = ({ children, className }) => {
|
||||||
|
return <div className={cn(styles.root, className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardBox;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.root {
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
font-size: var(--v2-font-size-3);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import cn from "classnames";
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import styles from "./DashboardComponentHeading.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardComponentHeading: FunctionComponent<Props> = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
return <h3 className={cn(styles.root, className)}>{children}</h3>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardComponentHeading;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import { Flex, Spinner } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import styles from "./Loader.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading: boolean;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Loader: FunctionComponent<Props> = ({ loading, height }) => {
|
||||||
|
if (!loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
style={height ? { height } : {}}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
className={styles.root}
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loader;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.timestamp {
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
font-weight: var(--v2-font-weight-primary-regular);
|
||||||
|
font-size: var(--v2-font-size-1);
|
||||||
|
color: var(--v2-colors-mono-500);
|
||||||
|
margin-top: var(--v2-spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Localized } from "@fluent/react/compat";
|
||||||
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { AbsoluteTime } from "coral-ui/components";
|
||||||
|
|
||||||
|
import styles from "./SiteDashboardTimestamp.css";
|
||||||
|
|
||||||
|
const SiteDashboardHeader: FunctionComponent = () => {
|
||||||
|
const [updatedAt, setUpdatedAt] = useState<Date | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
setUpdatedAt(new Date());
|
||||||
|
}, []);
|
||||||
|
if (!updatedAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<p className={styles.timestamp}>
|
||||||
|
<Localized id="dashboard-heading-last-updated">
|
||||||
|
<span>Last updated: </span>
|
||||||
|
</Localized>{" "}
|
||||||
|
<AbsoluteTime date={updatedAt.toISOString()} />
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SiteDashboardHeader;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.valueBoxCompareValue {
|
||||||
|
font-size: var(--v2-font-size-2);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueBoxCompareName {
|
||||||
|
font-size: var(--v2-font-size-1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import { HorizontalGutter } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import styles from "./TodayCompareValue.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodayCompareValue: FunctionComponent<Props> = ({
|
||||||
|
value = "-",
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<HorizontalGutter spacing={1}>
|
||||||
|
<p className={styles.valueBoxCompareValue}>{value}</p>
|
||||||
|
<p className={styles.valueBoxCompareName}>{children}</p>
|
||||||
|
</HorizontalGutter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodayCompareValue;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
.root {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tealIcon {
|
||||||
|
background-color: var(--v2-colors-teal-100);
|
||||||
|
color: var(--v2-colors-teal-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.redIcon {
|
||||||
|
background-color: var(--v2-colors-red-100);
|
||||||
|
color: var(--v2-colors-red-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.greyIcon {
|
||||||
|
background-color: var(--v2-colors-grey-200);
|
||||||
|
color: var(--v2-colors-grey-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blueIcon {
|
||||||
|
background-color: var(--v2-colors-blue-100);
|
||||||
|
color: var(--v2-colors-blue-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.outer {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import cn from "classnames";
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import { Flex, HorizontalGutter, Icon } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import DashboardBox from "./DashboardBox";
|
||||||
|
import Loader from "./Loader";
|
||||||
|
|
||||||
|
import styles from "./TodayDashboardBox.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon: "forum" | "close" | "badge" | "person_add" | "block";
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodayDashboardBox: FunctionComponent<Props> = ({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
loading,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<DashboardBox className={styles.root}>
|
||||||
|
{loading ? (
|
||||||
|
<Loader loading={loading} />
|
||||||
|
) : (
|
||||||
|
<Flex spacing={5} className={styles.outer}>
|
||||||
|
<div
|
||||||
|
className={cn(styles.icon, {
|
||||||
|
[styles.tealIcon]: icon === "forum",
|
||||||
|
[styles.redIcon]: icon === "close" || icon === "block",
|
||||||
|
[styles.greyIcon]: icon === "badge",
|
||||||
|
[styles.blueIcon]: icon === "person_add",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Icon size="md">{icon}</Icon>
|
||||||
|
</div>
|
||||||
|
<HorizontalGutter>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
className={styles.inner}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
</HorizontalGutter>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</DashboardBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodayDashboardBox;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.valueBoxValue {
|
||||||
|
font-size: var(--v2-font-size-7);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
line-height: var(--v2-line-height-reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueBoxName {
|
||||||
|
font-size: var(--v2-font-size-2);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import { HorizontalGutter } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import styles from "./TodayValue.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodayValue: FunctionComponent<Props> = ({ value = "-", children }) => {
|
||||||
|
return (
|
||||||
|
<HorizontalGutter spacing={1}>
|
||||||
|
<p className={styles.valueBoxValue}>{value}</p>
|
||||||
|
<p className={styles.valueBoxName}>{children}</p>
|
||||||
|
</HorizontalGutter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodayValue;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export { default as DashboardBox } from "./DashboardBox";
|
||||||
|
export { default as TodayCompareValue } from "./TodayCompareValue";
|
||||||
|
export { default as TodayValue } from "./TodayValue";
|
||||||
|
export { default as TodayDashboardBox } from "./TodayDashboardBox";
|
||||||
|
export { default as SiteDashboardHeader } from "./SiteDashboardTimestamp";
|
||||||
|
export { default as DashboardComponentHeading } from "./DashboardComponentHeading";
|
||||||
|
export { default as Loader } from "./Loader";
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Environment } from "relay-runtime";
|
||||||
|
|
||||||
|
import { createFetch } from "coral-framework/lib/relay";
|
||||||
|
|
||||||
|
export default function createDashboardFetch<T>(name: string, url: string) {
|
||||||
|
return createFetch(
|
||||||
|
name,
|
||||||
|
async (
|
||||||
|
environment: Environment,
|
||||||
|
variables: { siteID: string },
|
||||||
|
{ rest }
|
||||||
|
) => {
|
||||||
|
const params = new URLSearchParams(variables);
|
||||||
|
|
||||||
|
const { timeZone } = Intl.DateTimeFormat().resolvedOptions();
|
||||||
|
params.set("tz", timeZone);
|
||||||
|
|
||||||
|
// // FIXME: remove, date forced for development
|
||||||
|
// params.set("date", "2020-05-05T12:30:00.000Z");
|
||||||
|
|
||||||
|
return rest.fetch<T>(`${url}?${params.toString()}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default, default as DashboardRoute } from "./DashboardRoute";
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export const CHART_COLOR_PRIMARY = "#419EA7";
|
||||||
|
export const CHART_COLOR_PRIMARY_LIGHT = "#59C3C3";
|
||||||
|
export const CHART_COLOR_SECONDARY = "#F77160";
|
||||||
|
export const CHART_COLOR_MONO_500 = "#353F44";
|
||||||
|
export const CHART_COLOR_MONO_100 = "#65696B";
|
||||||
|
export const CHART_COLOR_GREY_200 = "#EAEFF0";
|
||||||
|
export const CHART_COLOR_PRIMARY_PALE = "#9FECDF";
|
||||||
|
export const CHART_COLOR_PRIMARY_DARK = "#2C7B8C";
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.heading {
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
font-size: var(--v2-font-size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
color: var(--v2-colors-coral-500);
|
||||||
|
font-size: var(--v2-font-size-2);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
margin-bottom: 0;
|
||||||
|
position: relative;
|
||||||
|
padding-left: var(--v2-spacing-6);
|
||||||
|
&:before {
|
||||||
|
width: 24px;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--v2-colors-coral-500);
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { Localized } from "@fluent/react/compat";
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
import {
|
||||||
|
CartesianGrid,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ReferenceLine,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
TooltipProps,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
|
import { Flex } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import { TimeSeriesMetricsJSON } from "coral-common/rest/dashboard/types";
|
||||||
|
import { useImmediateFetch } from "coral-framework/lib/relay/fetch";
|
||||||
|
import { useUIContext } from "coral-ui/components";
|
||||||
|
|
||||||
|
import { DashboardBox, DashboardComponentHeading, Loader } from "../components";
|
||||||
|
import createDashboardFetch from "../createDashboardFetch";
|
||||||
|
import {
|
||||||
|
CHART_COLOR_GREY_200,
|
||||||
|
CHART_COLOR_MONO_500,
|
||||||
|
CHART_COLOR_PRIMARY,
|
||||||
|
CHART_COLOR_SECONDARY,
|
||||||
|
} from "./ChartColors";
|
||||||
|
import CommentActivityTooltip from "./CommentActivityTooltip";
|
||||||
|
|
||||||
|
import styles from "./CommentActivity.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
locales?: string[];
|
||||||
|
siteID: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HourlyCommentsMetricsFetch = createDashboardFetch<TimeSeriesMetricsJSON>(
|
||||||
|
"hourlyCommentsMetricsFetch",
|
||||||
|
"/dashboard/hourly/comments"
|
||||||
|
);
|
||||||
|
|
||||||
|
const CommentActivity: FunctionComponent<Props> = ({
|
||||||
|
locales: localesFromProps,
|
||||||
|
siteID,
|
||||||
|
lastUpdated,
|
||||||
|
}) => {
|
||||||
|
const [hourly, loading] = useImmediateFetch(
|
||||||
|
HourlyCommentsMetricsFetch,
|
||||||
|
{ siteID },
|
||||||
|
lastUpdated
|
||||||
|
);
|
||||||
|
const { locales: localesFromContext } = useUIContext();
|
||||||
|
const locales = localesFromProps || localesFromContext || ["en-US"];
|
||||||
|
return (
|
||||||
|
<DashboardBox>
|
||||||
|
<Localized id="dashboard-comment-activity-heading">
|
||||||
|
<DashboardComponentHeading>
|
||||||
|
Hourly comment activity
|
||||||
|
</DashboardComponentHeading>
|
||||||
|
</Localized>
|
||||||
|
<Loader loading={loading} height={300} />
|
||||||
|
{!loading && (
|
||||||
|
<>
|
||||||
|
<ResponsiveContainer height={300}>
|
||||||
|
<LineChart
|
||||||
|
className={styles.chart}
|
||||||
|
data={hourly ? hourly.series : []}
|
||||||
|
>
|
||||||
|
{hourly && (
|
||||||
|
<ReferenceLine
|
||||||
|
stroke={CHART_COLOR_SECONDARY}
|
||||||
|
y={hourly.average}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<XAxis
|
||||||
|
dataKey="timestamp"
|
||||||
|
stroke={CHART_COLOR_MONO_500}
|
||||||
|
axisLine={{ strokeWidth: 0 }}
|
||||||
|
tick={{ fontSize: 12, fontWeight: 600 }}
|
||||||
|
tickLine={false}
|
||||||
|
dy={6}
|
||||||
|
tickFormatter={(unixTime: number) => {
|
||||||
|
const formatter = new Intl.DateTimeFormat(locales, {
|
||||||
|
hour: "numeric",
|
||||||
|
hour12: true,
|
||||||
|
});
|
||||||
|
return formatter
|
||||||
|
.format(new Date(unixTime))
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(" ", "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
allowDecimals={false}
|
||||||
|
tickLine={false}
|
||||||
|
width={36}
|
||||||
|
stroke={CHART_COLOR_MONO_500}
|
||||||
|
axisLine={{ strokeWidth: 0 }}
|
||||||
|
tick={{ fontSize: 12, fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
<CartesianGrid vertical={false} stroke={CHART_COLOR_GREY_200} />
|
||||||
|
<Line
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ strokeWidth: 1 }}
|
||||||
|
type="monotoneX"
|
||||||
|
dataKey="count"
|
||||||
|
stroke={CHART_COLOR_PRIMARY}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
content={(tooltipProps: TooltipProps) => (
|
||||||
|
<CommentActivityTooltip {...tooltipProps} locales={locales} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
<Flex alignItems="center" justifyContent="center">
|
||||||
|
<Localized id="dashboard-comment-activity-legend">
|
||||||
|
<p className={styles.legend}>All-time average</p>
|
||||||
|
</Localized>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DashboardBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentActivity;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.root {
|
||||||
|
background: var(--v2-colors-pure-white);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--v2-colors-grey-300);
|
||||||
|
padding: var(--v2-spacing-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
color: var(--v2-colors-mono-100);
|
||||||
|
font-size: var(--v2-font-size-2);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
color: var(--v2-colors-teal-600);
|
||||||
|
font-size: var(--v2-font-size-4);
|
||||||
|
font-weight: var(--v2-font-weight-primary-bold);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
color: var(--v2-colors-mono-500);
|
||||||
|
font-size: var(--v2-font-size-1);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Localized } from "@fluent/react/compat";
|
||||||
|
import React, { FunctionComponent, useMemo } from "react";
|
||||||
|
import { TooltipProps } from "recharts";
|
||||||
|
|
||||||
|
import styles from "./CommentActivityTooltip.css";
|
||||||
|
|
||||||
|
type Props = TooltipProps & {
|
||||||
|
locales: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommentActivityTooltip: FunctionComponent<Props> = ({
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
label,
|
||||||
|
locales,
|
||||||
|
}) => {
|
||||||
|
const formattedLabel = useMemo(() => {
|
||||||
|
if (label) {
|
||||||
|
const formatter = new Intl.DateTimeFormat(locales, {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
return formatter.format(new Date(label)).toLowerCase();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}, [label]);
|
||||||
|
if (active) {
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<p className={styles.time}>{formattedLabel}</p>
|
||||||
|
{payload && <p className={styles.count}>{payload[0].value}</p>}
|
||||||
|
<Localized id="dashboard-comment-activity-tooltip-comments">
|
||||||
|
<p className={styles.comments}>Comments</p>
|
||||||
|
</Localized>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentActivityTooltip;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.heading {
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
font-size: var(--v2-font-size-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { Localized } from "@fluent/react/compat";
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
ResponsiveContainer,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
|
||||||
|
import { TimeSeriesMetricsJSON } from "coral-common/rest/dashboard/types";
|
||||||
|
import { useImmediateFetch } from "coral-framework/lib/relay/fetch";
|
||||||
|
import { useUIContext } from "coral-ui/components";
|
||||||
|
|
||||||
|
import { DashboardBox, DashboardComponentHeading, Loader } from "../components";
|
||||||
|
import createDashboardFetch from "../createDashboardFetch";
|
||||||
|
import {
|
||||||
|
CHART_COLOR_GREY_200,
|
||||||
|
CHART_COLOR_MONO_500,
|
||||||
|
CHART_COLOR_PRIMARY_LIGHT,
|
||||||
|
CHART_COLOR_PRIMARY_PALE,
|
||||||
|
} from "./ChartColors";
|
||||||
|
import SignupActivityTick from "./SignupActivityTick";
|
||||||
|
|
||||||
|
import styles from "./SignupActivity.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
locales?: string[];
|
||||||
|
siteID: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
const DailySignupMetrics = createDashboardFetch<TimeSeriesMetricsJSON>(
|
||||||
|
"commenterActivityFetch",
|
||||||
|
"/dashboard/daily/users"
|
||||||
|
);
|
||||||
|
|
||||||
|
const CommenterActivity: FunctionComponent<Props> = ({
|
||||||
|
locales: localesFromProps,
|
||||||
|
siteID,
|
||||||
|
lastUpdated,
|
||||||
|
}) => {
|
||||||
|
const [daily, loading] = useImmediateFetch(
|
||||||
|
DailySignupMetrics,
|
||||||
|
{ siteID },
|
||||||
|
lastUpdated
|
||||||
|
);
|
||||||
|
const { locales: localesFromContext } = useUIContext();
|
||||||
|
const locales = localesFromProps || localesFromContext || ["en-US"];
|
||||||
|
return (
|
||||||
|
<DashboardBox>
|
||||||
|
<Localized id="dashboard-commenters-activity-heading">
|
||||||
|
<DashboardComponentHeading>New Signups</DashboardComponentHeading>
|
||||||
|
</Localized>
|
||||||
|
<Loader loading={loading} height={300} />
|
||||||
|
{!loading && (
|
||||||
|
<ResponsiveContainer height={300}>
|
||||||
|
<BarChart
|
||||||
|
className={styles.chart}
|
||||||
|
width={730}
|
||||||
|
height={250}
|
||||||
|
data={daily ? daily.series : []}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} stroke={CHART_COLOR_GREY_200} />
|
||||||
|
<XAxis
|
||||||
|
height={36}
|
||||||
|
stroke={CHART_COLOR_MONO_500}
|
||||||
|
axisLine={{ strokeWidth: 0 }}
|
||||||
|
tickLine={false}
|
||||||
|
dataKey="timestamp"
|
||||||
|
interval={0}
|
||||||
|
tick={(props) => (
|
||||||
|
<SignupActivityTick
|
||||||
|
isToday={
|
||||||
|
daily &&
|
||||||
|
daily.series &&
|
||||||
|
daily.series.length - 1 === props.index
|
||||||
|
}
|
||||||
|
locales={locales}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
allowDecimals={false}
|
||||||
|
tickLine={false}
|
||||||
|
width={36}
|
||||||
|
stroke={CHART_COLOR_MONO_500}
|
||||||
|
axisLine={{ strokeWidth: 0 }}
|
||||||
|
tick={{ fontSize: 12, fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="count">
|
||||||
|
{(daily ? daily.series : []).map((entry, index) => {
|
||||||
|
return (
|
||||||
|
<Cell
|
||||||
|
key={entry.timestamp}
|
||||||
|
fill={
|
||||||
|
daily && daily.series && daily.series.length - 1 === index
|
||||||
|
? CHART_COLOR_PRIMARY_PALE
|
||||||
|
: CHART_COLOR_PRIMARY_LIGHT
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
|
</DashboardBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommenterActivity;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React, { FunctionComponent, useMemo } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CHART_COLOR_MONO_100,
|
||||||
|
CHART_COLOR_MONO_500,
|
||||||
|
CHART_COLOR_PRIMARY_DARK,
|
||||||
|
} from "./ChartColors";
|
||||||
|
|
||||||
|
interface TickPayload {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
x: number;
|
||||||
|
locales: string[];
|
||||||
|
y: number;
|
||||||
|
payload: TickPayload;
|
||||||
|
isToday: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignupActivityTick: FunctionComponent<Props> = ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
payload,
|
||||||
|
locales,
|
||||||
|
isToday,
|
||||||
|
}) => {
|
||||||
|
const date = useMemo(() => {
|
||||||
|
const formatter = new Intl.DateTimeFormat(locales, {
|
||||||
|
day: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
});
|
||||||
|
return formatter.format(new Date(payload.value));
|
||||||
|
}, [payload.value]);
|
||||||
|
const dayOfWeek = useMemo(() => {
|
||||||
|
const formatter = new Intl.DateTimeFormat(locales, {
|
||||||
|
weekday: "short",
|
||||||
|
});
|
||||||
|
return formatter.format(new Date(payload.value));
|
||||||
|
}, [payload.value]);
|
||||||
|
return (
|
||||||
|
<g transform={`translate(${x},${y})`}>
|
||||||
|
<text
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
dy={12}
|
||||||
|
fill={isToday ? CHART_COLOR_PRIMARY_DARK : CHART_COLOR_MONO_500}
|
||||||
|
fontSize={12}
|
||||||
|
fontWeight={isToday ? 700 : 600}
|
||||||
|
textAnchor="middle"
|
||||||
|
>
|
||||||
|
{date}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
dy={28}
|
||||||
|
fill={isToday ? CHART_COLOR_PRIMARY_DARK : CHART_COLOR_MONO_100}
|
||||||
|
fontSize={12}
|
||||||
|
fontWeight={isToday ? 700 : 500}
|
||||||
|
textAnchor="middle"
|
||||||
|
>
|
||||||
|
{dayOfWeek}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignupActivityTick;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
.heading {
|
||||||
|
font-family: var(--v2-font-family-primary);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
font-size: var(--v2-font-size-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueBoxValue {
|
||||||
|
font-size: var(--v2-font-size-7);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
line-height: var(--v2-line-height-reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueBoxName {
|
||||||
|
font-size: var(--v2-font-size-2);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueBoxCompareValue {
|
||||||
|
font-size: var(--v2-font-size-2);
|
||||||
|
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.valueBoxCompareName {
|
||||||
|
font-size: var(--v2-font-size-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forumIcon {
|
||||||
|
background-color: var(--v2-colors-teal-100);
|
||||||
|
color: var(--v2-colors-teal-600);
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { Localized } from "@fluent/react/compat";
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import { TodayMetricsJSON } from "coral-common/rest/dashboard/types";
|
||||||
|
import { useImmediateFetch } from "coral-framework/lib/relay/fetch";
|
||||||
|
import { Flex } from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TodayCompareValue,
|
||||||
|
TodayDashboardBox,
|
||||||
|
TodayValue,
|
||||||
|
} from "../components";
|
||||||
|
import createDashboardFetch from "../createDashboardFetch";
|
||||||
|
|
||||||
|
import styles from "./Today.css";
|
||||||
|
|
||||||
|
const TodayMetricsFetch = createDashboardFetch<TodayMetricsJSON>(
|
||||||
|
"todayMetricsFetch",
|
||||||
|
"/dashboard/today"
|
||||||
|
);
|
||||||
|
|
||||||
|
const TotalMetricsFetch = createDashboardFetch<TodayMetricsJSON>(
|
||||||
|
"totalMetricsFetch",
|
||||||
|
"/dashboard/total"
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
siteID: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TodayTotals: FunctionComponent<Props> = ({ siteID, lastUpdated }) => {
|
||||||
|
const [today, loading] = useImmediateFetch(
|
||||||
|
TodayMetricsFetch,
|
||||||
|
{ siteID },
|
||||||
|
lastUpdated
|
||||||
|
);
|
||||||
|
const [total, totalLoading] = useImmediateFetch(
|
||||||
|
TotalMetricsFetch,
|
||||||
|
{ siteID },
|
||||||
|
lastUpdated
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Localized id="dashboard-today-heading">
|
||||||
|
<h3 className={styles.heading}>Today's activity</h3>
|
||||||
|
</Localized>
|
||||||
|
<Flex justifyContent="space-between" spacing={3}>
|
||||||
|
<TodayDashboardBox icon="forum" loading={loading || totalLoading}>
|
||||||
|
<TodayValue value={today?.comments.total.toString()}>
|
||||||
|
<Localized id="dashboard-today-new-comments">
|
||||||
|
New comments
|
||||||
|
</Localized>
|
||||||
|
</TodayValue>
|
||||||
|
<TodayCompareValue value={total?.comments.total.toString()}>
|
||||||
|
<Localized id="dashboard-alltime-new-comments">
|
||||||
|
All time total
|
||||||
|
</Localized>
|
||||||
|
</TodayCompareValue>
|
||||||
|
</TodayDashboardBox>
|
||||||
|
<TodayDashboardBox icon="close" loading={loading || totalLoading}>
|
||||||
|
<TodayValue
|
||||||
|
value={
|
||||||
|
today
|
||||||
|
? `${(today.comments.total > 0
|
||||||
|
? (today.comments.rejected / today.comments.total) * 100
|
||||||
|
: 0
|
||||||
|
).toFixed(2)} %`
|
||||||
|
: "-.-- %"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Localized id="dashboard-today-rejections">
|
||||||
|
Rejection rate
|
||||||
|
</Localized>
|
||||||
|
</TodayValue>
|
||||||
|
<TodayCompareValue
|
||||||
|
value={
|
||||||
|
total
|
||||||
|
? `${(total.comments.total > 0
|
||||||
|
? (total.comments.rejected / total.comments.total) * 100
|
||||||
|
: 0
|
||||||
|
).toFixed(2)} %`
|
||||||
|
: "-.-- %"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Localized id="dashboard-alltime-rejections">
|
||||||
|
All time average
|
||||||
|
</Localized>
|
||||||
|
</TodayCompareValue>
|
||||||
|
</TodayDashboardBox>
|
||||||
|
<TodayDashboardBox icon="badge" loading={loading || totalLoading}>
|
||||||
|
<TodayValue value={today?.comments.staff.toString()}>
|
||||||
|
<Localized id="dashboard-today-staff-comments">
|
||||||
|
Staff comments
|
||||||
|
</Localized>
|
||||||
|
</TodayValue>
|
||||||
|
<TodayCompareValue value={total?.comments.staff.toString()}>
|
||||||
|
<Localized id="dashboard-alltime-staff-comments">
|
||||||
|
All time total
|
||||||
|
</Localized>
|
||||||
|
</TodayCompareValue>
|
||||||
|
</TodayDashboardBox>
|
||||||
|
<TodayDashboardBox icon="person_add" loading={loading || totalLoading}>
|
||||||
|
<TodayValue value={today?.users.total.toString()}>
|
||||||
|
<Localized id="dashboard-today-signups">
|
||||||
|
New community members
|
||||||
|
</Localized>
|
||||||
|
</TodayValue>
|
||||||
|
<TodayCompareValue value={total?.users.total.toString()}>
|
||||||
|
<Localized id="dashboard-alltime-signups">Total members</Localized>
|
||||||
|
</TodayCompareValue>
|
||||||
|
</TodayDashboardBox>
|
||||||
|
<TodayDashboardBox icon="block" loading={loading || totalLoading}>
|
||||||
|
<TodayValue value={today?.users.bans.toString()}>
|
||||||
|
<Localized id="dashboard-today-bans">Banned members</Localized>
|
||||||
|
</TodayValue>
|
||||||
|
<TodayCompareValue value={total?.users.bans.toString()}>
|
||||||
|
<Localized id="dashboard-alltime-bans">
|
||||||
|
Total banned members
|
||||||
|
</Localized>
|
||||||
|
</TodayCompareValue>
|
||||||
|
</TodayDashboardBox>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodayTotals;
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Localized } from "@fluent/react/compat";
|
||||||
|
import { Link } from "found";
|
||||||
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import NotAvailable from "coral-admin/components/NotAvailable";
|
||||||
|
import { TodayStoriesMetricsJSON } from "coral-common/rest/dashboard/types";
|
||||||
|
import { getModerationLink } from "coral-framework/helpers";
|
||||||
|
import { useImmediateFetch } from "coral-framework/lib/relay/fetch";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TextLink,
|
||||||
|
} from "coral-ui/components/v2";
|
||||||
|
|
||||||
|
import { DashboardBox, DashboardComponentHeading, Loader } from "../components";
|
||||||
|
import createDashboardFetch from "../createDashboardFetch";
|
||||||
|
|
||||||
|
const TodayStoriesMetrics = createDashboardFetch<TodayStoriesMetricsJSON>(
|
||||||
|
"topStoriesFetch",
|
||||||
|
"/dashboard/today/stories"
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
siteID: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopStories: FunctionComponent<Props> = ({ siteID, lastUpdated }) => {
|
||||||
|
const [today, loading] = useImmediateFetch(
|
||||||
|
TodayStoriesMetrics,
|
||||||
|
{ siteID },
|
||||||
|
lastUpdated
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<DashboardBox>
|
||||||
|
<Localized id="dashboard-top-stories-today-heading">
|
||||||
|
<DashboardComponentHeading>
|
||||||
|
Today's most commented stories
|
||||||
|
</DashboardComponentHeading>
|
||||||
|
</Localized>
|
||||||
|
<Table fullWidth>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<Localized id="dashboard-top-stories-table-header-story">
|
||||||
|
<TableCell>Story</TableCell>
|
||||||
|
</Localized>
|
||||||
|
<Localized id="dashboard-top-stories-table-header-comments">
|
||||||
|
<TableCell align="end">Comments</TableCell>
|
||||||
|
</Localized>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{loading && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={2}>
|
||||||
|
<Loader loading={loading} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{today && today.results.length < 1 && (
|
||||||
|
<TableRow>
|
||||||
|
<Localized id="dashboard-top-stories-no-comments">
|
||||||
|
<TableCell colSpan={2}>No comments today.</TableCell>
|
||||||
|
</Localized>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{today &&
|
||||||
|
today.results.map((result) => (
|
||||||
|
<TableRow key={result.story.id}>
|
||||||
|
<TableCell>
|
||||||
|
<Link
|
||||||
|
to={getModerationLink({ storyID: result.story.id })}
|
||||||
|
as={TextLink}
|
||||||
|
>
|
||||||
|
{result.story.title ? result.story.title : <NotAvailable />}
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="end">{result.count}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</DashboardBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopStories;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as Today } from "./Today";
|
||||||
|
export { default as SignupActivity } from "./SignupActivity";
|
||||||
|
export { default as CommentActivity } from "./CommentActivity";
|
||||||
|
export { default as TopStories } from "./TopStories";
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback } from "react";
|
import { isUndefined } from "lodash";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { fetchQuery as relayFetchQuery } from "react-relay";
|
import { fetchQuery as relayFetchQuery } from "react-relay";
|
||||||
import {
|
import {
|
||||||
compose,
|
compose,
|
||||||
@@ -70,6 +71,34 @@ export function useFetch<V, R>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useImmediateFetch<V extends {}, R>(
|
||||||
|
fetch: Fetch<any, V, Promise<R>>,
|
||||||
|
variables: V,
|
||||||
|
refetch?: string
|
||||||
|
): [R | null, boolean] {
|
||||||
|
const fetcher = useFetch(fetch);
|
||||||
|
const [state, setState] = useState<R | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function doTheFetch() {
|
||||||
|
setState(null);
|
||||||
|
setLoading(true);
|
||||||
|
const value = await fetcher(variables);
|
||||||
|
|
||||||
|
// TODO: Maybe we don't need this timeout?
|
||||||
|
setTimeout(() => {
|
||||||
|
setState(value);
|
||||||
|
setLoading(false);
|
||||||
|
}, 100 + 50 * Math.random());
|
||||||
|
}
|
||||||
|
|
||||||
|
doTheFetch();
|
||||||
|
}, Object.values(variables).concat(isUndefined(refetch) ? [] : [refetch]));
|
||||||
|
|
||||||
|
return [state, loading];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* withFetch creates a HOC that injects the fetch as
|
* withFetch creates a HOC that injects the fetch as
|
||||||
* a property.
|
* a property.
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
import { Flex } from "coral-ui/components";
|
||||||
|
import Icon from "coral-ui/components/Icon";
|
||||||
|
import BaseButton, { BaseButtonProps } from "coral-ui/components/v2/BaseButton";
|
||||||
import { withStyles } from "coral-ui/hocs";
|
import { withStyles } from "coral-ui/hocs";
|
||||||
|
|
||||||
import BaseButton, { BaseButtonProps } from "coral-ui/components/BaseButton";
|
|
||||||
import Icon from "coral-ui/components/Icon";
|
|
||||||
|
|
||||||
import { Flex } from "coral-ui/components";
|
|
||||||
import styles from "./Button.css";
|
import styles from "./Button.css";
|
||||||
|
|
||||||
interface Props extends Omit<BaseButtonProps, "ref"> {
|
interface Props extends Omit<BaseButtonProps, "ref"> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
to?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: React.EventHandler<React.MouseEvent>;
|
onClick?: React.EventHandler<React.MouseEvent>;
|
||||||
classes: typeof styles;
|
classes: typeof styles;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export interface TodayMetricsJSON {
|
||||||
|
users: {
|
||||||
|
total: number;
|
||||||
|
bans: number;
|
||||||
|
};
|
||||||
|
comments: {
|
||||||
|
total: number;
|
||||||
|
rejected: number;
|
||||||
|
staff: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSeriesMetricsJSON {
|
||||||
|
series: Array<{ count: number; timestamp: string }>;
|
||||||
|
average?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TodayStoriesMetricsJSON {
|
||||||
|
results: Array<{
|
||||||
|
count: number;
|
||||||
|
story: {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import {
|
||||||
|
TimeSeriesMetricsJSON,
|
||||||
|
TodayMetricsJSON,
|
||||||
|
TodayStoriesMetricsJSON,
|
||||||
|
} from "coral-common/rest/dashboard/types";
|
||||||
|
import { AppOptions } from "coral-server/app";
|
||||||
|
import { calculateTotalCommentCount } from "coral-server/models/comment";
|
||||||
|
import {
|
||||||
|
retrieveAllTimeStaffCommentMetrics,
|
||||||
|
retrieveAverageCommentsMetric,
|
||||||
|
retrieveHourlyCommentMetrics,
|
||||||
|
retrieveTodayCommentMetrics,
|
||||||
|
retrieveTodayTopStoryMetrics,
|
||||||
|
} from "coral-server/models/comment/metrics";
|
||||||
|
import { retrieveSite } from "coral-server/models/site";
|
||||||
|
import { retrieveManyStories } from "coral-server/models/story";
|
||||||
|
import {
|
||||||
|
retrieveAllTimeUserMetrics,
|
||||||
|
retrieveDailyUserMetrics,
|
||||||
|
retrieveTodayUserMetrics,
|
||||||
|
} from "coral-server/models/user/metrics";
|
||||||
|
import { Request, RequestHandler } from "coral-server/types/express";
|
||||||
|
|
||||||
|
function getMetricsOptions(req: Request) {
|
||||||
|
// Get the current Tenant on the request.
|
||||||
|
const { id: tenantID } = req.coral?.tenant!;
|
||||||
|
|
||||||
|
const now = req.coral?.now!;
|
||||||
|
|
||||||
|
// To set a fixed date for the date, uncomment the line below.
|
||||||
|
// const now = DateTime.utc(2020, 5, 5, 12, 30).toJSDate();
|
||||||
|
|
||||||
|
// To allow for date overrides (to load metrics for another time then now),
|
||||||
|
// uncomment the lines below.
|
||||||
|
// const { date = req.coral?.now!.toISOString() } = req.query;
|
||||||
|
// const now = new Date(date);
|
||||||
|
|
||||||
|
// Get the Site ID and timezone for this set of metrics.
|
||||||
|
const { siteID, tz } = req.query;
|
||||||
|
if (!tz) {
|
||||||
|
throw new Error("tz was not provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tenantID, siteID, tz, now };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const todayMetricsHandler = ({
|
||||||
|
mongo,
|
||||||
|
}: AppOptions): RequestHandler => async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { tenantID, siteID, tz, now } = getMetricsOptions(req);
|
||||||
|
if (!siteID) {
|
||||||
|
throw new Error("siteID was not provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [users, comments] = await Promise.all([
|
||||||
|
retrieveTodayUserMetrics(mongo, tenantID, tz, now),
|
||||||
|
retrieveTodayCommentMetrics(mongo, tenantID, siteID, tz, now),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result: TodayMetricsJSON = {
|
||||||
|
users,
|
||||||
|
comments,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const totalMetricsHandler = ({
|
||||||
|
mongo,
|
||||||
|
}: AppOptions): RequestHandler => async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { tenantID, siteID } = getMetricsOptions(req);
|
||||||
|
if (!siteID) {
|
||||||
|
throw new Error("siteID was not provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const site = await retrieveSite(mongo, tenantID, siteID);
|
||||||
|
if (!site) {
|
||||||
|
throw new Error("site specified was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [users, staff] = await Promise.all([
|
||||||
|
retrieveAllTimeUserMetrics(mongo, tenantID),
|
||||||
|
retrieveAllTimeStaffCommentMetrics(mongo, tenantID, siteID),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result: TodayMetricsJSON = {
|
||||||
|
users,
|
||||||
|
comments: {
|
||||||
|
total: calculateTotalCommentCount(site.commentCounts.status),
|
||||||
|
rejected: site.commentCounts.status.REJECTED,
|
||||||
|
staff,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hourlyCommentsMetricsHandler = ({
|
||||||
|
mongo,
|
||||||
|
}: AppOptions): RequestHandler => async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { tenantID, siteID, tz, now } = getMetricsOptions(req);
|
||||||
|
if (!siteID) {
|
||||||
|
throw new Error("siteID was not provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [series, average] = await Promise.all([
|
||||||
|
retrieveHourlyCommentMetrics(mongo, tenantID, siteID, tz, now),
|
||||||
|
retrieveAverageCommentsMetric(mongo, tenantID, siteID, tz, now),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result: TimeSeriesMetricsJSON = {
|
||||||
|
series,
|
||||||
|
average,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dailyUsersMetricsHandler = ({
|
||||||
|
mongo,
|
||||||
|
}: AppOptions): RequestHandler => async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { tenantID, tz, now } = getMetricsOptions(req);
|
||||||
|
|
||||||
|
const result: TimeSeriesMetricsJSON = {
|
||||||
|
series: await retrieveDailyUserMetrics(mongo, tenantID, tz, now),
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const todayStoriesMetricsHandler = ({
|
||||||
|
mongo,
|
||||||
|
}: AppOptions): RequestHandler => async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { tenantID, siteID, tz, now } = getMetricsOptions(req);
|
||||||
|
if (!siteID) {
|
||||||
|
throw new Error("siteID was not provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await retrieveTodayTopStoryMetrics(
|
||||||
|
mongo,
|
||||||
|
tenantID,
|
||||||
|
siteID,
|
||||||
|
tz,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch all the stories for each count.
|
||||||
|
const stories = await retrieveManyStories(
|
||||||
|
mongo,
|
||||||
|
tenantID,
|
||||||
|
results.map(({ _id }) => _id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure that all entries are not null.
|
||||||
|
if (!stories.every((story) => story) || results.length !== stories.length) {
|
||||||
|
throw new Error("some stories with comments were not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: TodayStoriesMetricsJSON = {
|
||||||
|
results: results.map(({ count }, idx) => {
|
||||||
|
// We verified above that there were no null values.
|
||||||
|
const story = stories[idx]!;
|
||||||
|
|
||||||
|
return { count, story: { id: story.id, title: story.metadata?.title } };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,5 +4,6 @@ export * from "./graphql";
|
|||||||
export * from "./health";
|
export * from "./health";
|
||||||
export * from "./install";
|
export * from "./install";
|
||||||
export * from "./version";
|
export * from "./version";
|
||||||
|
export * from "./dashboard";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
export * from "./story";
|
export * from "./story";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { SSOToken, SSOVerifier } from "./verifiers/sso";
|
|||||||
|
|
||||||
export type JWTStrategyOptions = Pick<
|
export type JWTStrategyOptions = Pick<
|
||||||
AppOptions,
|
AppOptions,
|
||||||
"signingConfig" | "mongo" | "redis" | "tenantCache"
|
"signingConfig" | "mongo" | "redis" | "tenantCache" | "mongo"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Db } from "mongodb";
|
import { Db } from "mongodb";
|
||||||
import { Strategy as BaseStrategy, StrategyCreated } from "passport";
|
import { Profile, Strategy as BaseStrategy, StrategyCreated } from "passport";
|
||||||
|
import { VerifyCallback } from "passport-oauth2";
|
||||||
import { Strategy } from "passport-strategy";
|
import { Strategy } from "passport-strategy";
|
||||||
|
|
||||||
import { Config } from "coral-server/config";
|
import { Config } from "coral-server/config";
|
||||||
@@ -12,8 +13,6 @@ import {
|
|||||||
TenantCacheAdapter,
|
TenantCacheAdapter,
|
||||||
} from "coral-server/services/tenant/cache";
|
} from "coral-server/services/tenant/cache";
|
||||||
import { Request } from "coral-server/types/express";
|
import { Request } from "coral-server/types/express";
|
||||||
import { Profile } from "passport";
|
|
||||||
import { VerifyCallback } from "passport-oauth2";
|
|
||||||
|
|
||||||
interface OAuth2Integration {
|
interface OAuth2Integration {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ import {
|
|||||||
|
|
||||||
export { OIDCIDToken } from "../oidc";
|
export { OIDCIDToken } from "../oidc";
|
||||||
|
|
||||||
export type OIDCVerifierOptions = Pick<
|
export type OIDCVerifierOptions = Pick<AppOptions, "mongo" | "tenantCache">;
|
||||||
AppOptions,
|
|
||||||
"mongo" | "redis" | "tenantCache"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export class OIDCVerifier implements Verifier<OIDCIDToken> {
|
export class OIDCVerifier implements Verifier<OIDCIDToken> {
|
||||||
private mongo: Db;
|
private mongo: Db;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { UserForbiddenError } from "coral-server/errors";
|
||||||
|
import { RequestHandler } from "coral-server/types/express";
|
||||||
|
|
||||||
|
import { GQLUSER_ROLE } from "coral-server/graph/schema/__generated__/types";
|
||||||
|
|
||||||
|
export const roleMiddleware = (roles: GQLUSER_ROLE[]): RequestHandler => (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
next
|
||||||
|
) => {
|
||||||
|
if (!req.user || !roles.includes(req.user.role)) {
|
||||||
|
return next(
|
||||||
|
new UserForbiddenError(
|
||||||
|
"user does not have sufficient privileges",
|
||||||
|
req.originalUrl,
|
||||||
|
req.method
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
|
import { RequestLimiter } from "coral-server/app/request/limiter";
|
||||||
|
import { Config } from "coral-server/config";
|
||||||
|
import { RequestHandler } from "coral-server/types/express";
|
||||||
|
|
||||||
|
export interface MiddlewareOptions {
|
||||||
|
redis: Redis;
|
||||||
|
config: Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userLimiterMiddleware = ({
|
||||||
|
redis,
|
||||||
|
config,
|
||||||
|
}: MiddlewareOptions): RequestHandler => {
|
||||||
|
const limiter = new RequestLimiter({
|
||||||
|
redis,
|
||||||
|
ttl: "1m",
|
||||||
|
max: 5,
|
||||||
|
prefix: "userID",
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
return async (req, res, next) => {
|
||||||
|
limiter
|
||||||
|
.test(req, req.user!.id)
|
||||||
|
.then(() => next())
|
||||||
|
.catch((err) => next(err));
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { AppOptions } from "coral-server/app";
|
||||||
|
import {
|
||||||
|
dailyUsersMetricsHandler,
|
||||||
|
hourlyCommentsMetricsHandler,
|
||||||
|
todayMetricsHandler,
|
||||||
|
todayStoriesMetricsHandler,
|
||||||
|
totalMetricsHandler,
|
||||||
|
} from "coral-server/app/handlers";
|
||||||
|
import { userLimiterMiddleware } from "coral-server/app/middleware/userLimiter";
|
||||||
|
|
||||||
|
import { createAPIRouter } from "./helpers";
|
||||||
|
|
||||||
|
export function createDashboardRouter(app: AppOptions) {
|
||||||
|
const router = createAPIRouter({ cache: "30s" });
|
||||||
|
|
||||||
|
router.use(userLimiterMiddleware(app));
|
||||||
|
|
||||||
|
router.get("/today", todayMetricsHandler(app));
|
||||||
|
router.get("/total", totalMetricsHandler(app));
|
||||||
|
router.get("/hourly/comments", hourlyCommentsMetricsHandler(app));
|
||||||
|
router.get("/daily/users", dailyUsersMetricsHandler(app));
|
||||||
|
router.get("/today/stories", todayStoriesMetricsHandler(app));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
@@ -6,13 +6,17 @@ import { graphQLHandler } from "coral-server/app/handlers";
|
|||||||
import { JSONErrorHandler } from "coral-server/app/middleware/error";
|
import { JSONErrorHandler } from "coral-server/app/middleware/error";
|
||||||
import { persistedQueryMiddleware } from "coral-server/app/middleware/graphql";
|
import { persistedQueryMiddleware } from "coral-server/app/middleware/graphql";
|
||||||
import { jsonMiddleware } from "coral-server/app/middleware/json";
|
import { jsonMiddleware } from "coral-server/app/middleware/json";
|
||||||
|
import { loggedInMiddleware } from "coral-server/app/middleware/loggedIn";
|
||||||
import { errorLogger } from "coral-server/app/middleware/logging";
|
import { errorLogger } from "coral-server/app/middleware/logging";
|
||||||
import { notFoundMiddleware } from "coral-server/app/middleware/notFound";
|
import { notFoundMiddleware } from "coral-server/app/middleware/notFound";
|
||||||
import { authenticate } from "coral-server/app/middleware/passport";
|
import { authenticate } from "coral-server/app/middleware/passport";
|
||||||
|
import { roleMiddleware } from "coral-server/app/middleware/role";
|
||||||
import { tenantMiddleware } from "coral-server/app/middleware/tenant";
|
import { tenantMiddleware } from "coral-server/app/middleware/tenant";
|
||||||
|
import { STAFF_ROLES } from "coral-server/models/user/constants";
|
||||||
|
|
||||||
import { createNewAccountRouter } from "./account";
|
import { createNewAccountRouter } from "./account";
|
||||||
import { createNewAuthRouter } from "./auth";
|
import { createNewAuthRouter } from "./auth";
|
||||||
|
import { createDashboardRouter } from "./dashboard";
|
||||||
import { createNewInstallRouter } from "./install";
|
import { createNewInstallRouter } from "./install";
|
||||||
import { createStoryRouter } from "./story";
|
import { createStoryRouter } from "./story";
|
||||||
import { createNewUserRouter } from "./user";
|
import { createNewUserRouter } from "./user";
|
||||||
@@ -56,6 +60,14 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) {
|
|||||||
graphQLHandler(app)
|
graphQLHandler(app)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.use(
|
||||||
|
"/dashboard",
|
||||||
|
authenticate(options.passport),
|
||||||
|
loggedInMiddleware,
|
||||||
|
roleMiddleware(STAFF_ROLES),
|
||||||
|
createDashboardRouter(app)
|
||||||
|
);
|
||||||
|
|
||||||
// General API error handler.
|
// General API error handler.
|
||||||
router.use(notFoundMiddleware);
|
router.use(notFoundMiddleware);
|
||||||
router.use(errorLogger);
|
router.use(errorLogger);
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
export interface TimeRange {
|
||||||
|
readonly start: DateTime;
|
||||||
|
readonly end: DateTime;
|
||||||
|
readonly hours: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TimeUnit = "day" | "hour";
|
||||||
|
|
||||||
|
const TIME_UNIT_FORMAT: Record<TimeUnit, { js: string; mongo: string }> = {
|
||||||
|
day: { js: "yyyy-LL-dd", mongo: "%Y-%m-%d" },
|
||||||
|
hour: { js: "yyyy-LL-dd HH:00", mongo: "%Y-%m-%d %H:00" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIME_UNIT_HOURS: Record<TimeUnit, number> = {
|
||||||
|
day: 24,
|
||||||
|
hour: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TIME_UNIT_MAX: Record<TimeUnit, number> = {
|
||||||
|
day: 7,
|
||||||
|
hour: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTimeRange(
|
||||||
|
unit: TimeUnit,
|
||||||
|
zone: string,
|
||||||
|
now: Date,
|
||||||
|
interval = TIME_UNIT_MAX[unit]
|
||||||
|
): TimeRange {
|
||||||
|
// Convert the date to the specified zone.
|
||||||
|
const end = DateTime.fromJSDate(now).setZone(zone);
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: end.startOf(unit).minus({ [unit]: interval - 1 }),
|
||||||
|
end,
|
||||||
|
hours: interval * TIME_UNIT_HOURS[unit],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMongoFormat(unit: TimeUnit): string {
|
||||||
|
return TIME_UNIT_FORMAT[unit].mongo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
_id: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
count: number;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTimeRangeSeries(
|
||||||
|
unit: TimeUnit,
|
||||||
|
start: DateTime,
|
||||||
|
results: Result[]
|
||||||
|
) {
|
||||||
|
if (results.length > TIME_UNIT_MAX[unit]) {
|
||||||
|
throw new Error(
|
||||||
|
`invalid number of items, expected ${TIME_UNIT_MAX[unit]}, got ${results.length}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const series: Point[] = [];
|
||||||
|
|
||||||
|
let date = start;
|
||||||
|
for (let i = 0; i < TIME_UNIT_MAX[unit]; i++) {
|
||||||
|
const search = date.toFormat(TIME_UNIT_FORMAT[unit].js);
|
||||||
|
const result = results.find(({ _id }) => _id === search);
|
||||||
|
|
||||||
|
// Add the result (or zero if it doesn't exist) to the series.
|
||||||
|
series.push({
|
||||||
|
count: result ? result.count : 0,
|
||||||
|
timestamp: date.toJSDate().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Increment the date by the specified unit.
|
||||||
|
date = date.plus({ [unit]: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCount<T extends Array<{ count: number }>>(results: T) {
|
||||||
|
return results.length === 1 ? results[0].count : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { Db } from "mongodb";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatTimeRangeSeries,
|
||||||
|
getCount,
|
||||||
|
getMongoFormat,
|
||||||
|
getTimeRange,
|
||||||
|
Result,
|
||||||
|
} from "coral-server/helpers/metrics";
|
||||||
|
import { comments as collection } from "coral-server/services/mongodb/collections";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GQLCOMMENT_STATUS,
|
||||||
|
GQLTAG,
|
||||||
|
} from "coral-server/graph/schema/__generated__/types";
|
||||||
|
|
||||||
|
export async function retrieveHourlyCommentMetrics(
|
||||||
|
mongo: Db,
|
||||||
|
tenantID: string,
|
||||||
|
siteID: string,
|
||||||
|
timezone: string,
|
||||||
|
now: Date
|
||||||
|
) {
|
||||||
|
const { start, end } = getTimeRange("hour", timezone, now);
|
||||||
|
|
||||||
|
// Return the last 24 hours (in hour documents).
|
||||||
|
const results = await collection<Result>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { tenantID, siteID, createdAt: { $gte: start, $lte: end } } },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
$dateToString: {
|
||||||
|
date: "$createdAt",
|
||||||
|
format: getMongoFormat("hour"),
|
||||||
|
timezone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
count: { $sum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { _id: 1 } },
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return formatTimeRangeSeries("hour", start, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveTodayCommentMetrics(
|
||||||
|
mongo: Db,
|
||||||
|
tenantID: string,
|
||||||
|
siteID: string,
|
||||||
|
timezone: string,
|
||||||
|
now: Date
|
||||||
|
) {
|
||||||
|
const start = DateTime.fromJSDate(now).setZone(timezone).startOf("day");
|
||||||
|
const end = DateTime.fromJSDate(now);
|
||||||
|
|
||||||
|
const status = await collection<{ _id: GQLCOMMENT_STATUS; count: number }>(
|
||||||
|
mongo
|
||||||
|
)
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
tenantID,
|
||||||
|
siteID,
|
||||||
|
createdAt: { $gte: start, $lte: end },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$status",
|
||||||
|
count: { $sum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
const rejected = status.find((doc) => doc._id === GQLCOMMENT_STATUS.REJECTED);
|
||||||
|
const total = status.reduce((acc, doc) => acc + doc.count, 0);
|
||||||
|
|
||||||
|
const staff = await collection<{ count: number }>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
tenantID,
|
||||||
|
siteID,
|
||||||
|
createdAt: { $gte: start, $lte: end },
|
||||||
|
"tags.type": GQLTAG.STAFF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $count: "count" },
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
rejected: rejected ? rejected.count : 0,
|
||||||
|
staff: getCount(staff),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveAllTimeStaffCommentMetrics(
|
||||||
|
mongo: Db,
|
||||||
|
tenantID: string,
|
||||||
|
siteID: string
|
||||||
|
) {
|
||||||
|
// Get the referenced tenant, site, and staff comments.
|
||||||
|
const staff = await collection<{ count: number }>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
tenantID,
|
||||||
|
siteID,
|
||||||
|
"tags.type": GQLTAG.STAFF,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $count: "count" },
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return getCount(staff);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveAverageCommentsMetric(
|
||||||
|
mongo: Db,
|
||||||
|
tenantID: string,
|
||||||
|
siteID: string,
|
||||||
|
timezone: string,
|
||||||
|
now: Date
|
||||||
|
) {
|
||||||
|
const { start, hours, end } = getTimeRange("hour", timezone, now, 72);
|
||||||
|
|
||||||
|
// Return the last 24 hours (in hour documents).
|
||||||
|
const results = await collection<{ count: number }>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { tenantID, siteID, createdAt: { $gte: start, $lte: end } } },
|
||||||
|
{ $count: "count" },
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
const total = getCount(results);
|
||||||
|
|
||||||
|
return Math.floor(total / hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveTodayTopStoryMetrics(
|
||||||
|
mongo: Db,
|
||||||
|
tenantID: string,
|
||||||
|
siteID: string,
|
||||||
|
timezone: string,
|
||||||
|
now: Date
|
||||||
|
) {
|
||||||
|
const start = DateTime.fromJSDate(now).setZone(timezone).startOf("day");
|
||||||
|
const end = DateTime.fromJSDate(now);
|
||||||
|
|
||||||
|
// Return the last 24 hours worth of comments.
|
||||||
|
const results = await collection<Result>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { tenantID, siteID, createdAt: { $gte: start, $lte: end } } },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$storyID",
|
||||||
|
count: { $sum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { count: -1 } },
|
||||||
|
// TODO: 17 was for visual treatment, feel free to change this!
|
||||||
|
{ $limit: 17 },
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
@@ -121,6 +121,10 @@ async function retrieveConnection(
|
|||||||
return resolveConnection(query, input, (_, index) => index + skip + 1);
|
return resolveConnection(query, input, (_, index) => index + skip + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function countTenantSites(mongo: Db, tenantID: string) {
|
||||||
|
return collection(mongo).find({ tenantID }).count();
|
||||||
|
}
|
||||||
|
|
||||||
export async function retrieveSiteConnection(
|
export async function retrieveSiteConnection(
|
||||||
mongo: Db,
|
mongo: Db,
|
||||||
tenantID: string,
|
tenantID: string,
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { Db } from "mongodb";
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatTimeRangeSeries,
|
||||||
|
getCount,
|
||||||
|
getMongoFormat,
|
||||||
|
getTimeRange,
|
||||||
|
Result,
|
||||||
|
} from "coral-server/helpers/metrics";
|
||||||
|
import { users as collection } from "coral-server/services/mongodb/collections";
|
||||||
|
|
||||||
|
export async function retrieveTodayUserMetrics(
|
||||||
|
mongo: Db,
|
||||||
|
tenantID: string,
|
||||||
|
timezone: string,
|
||||||
|
now: Date
|
||||||
|
) {
|
||||||
|
const start = DateTime.fromJSDate(now).setZone(timezone).startOf("day");
|
||||||
|
const end = DateTime.fromJSDate(now);
|
||||||
|
|
||||||
|
const [total, bans] = await Promise.all([
|
||||||
|
collection<{ count: number }>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { tenantID, createdAt: { $gte: start, $lte: end } } },
|
||||||
|
{ $count: "count" },
|
||||||
|
])
|
||||||
|
.toArray(),
|
||||||
|
collection<{ count: number }>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
tenantID,
|
||||||
|
"status.ban.active": true,
|
||||||
|
"status.ban.history.createdAt": { $gte: start, $lte: end },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $count: "count" },
|
||||||
|
])
|
||||||
|
.toArray(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: getCount(total),
|
||||||
|
bans: getCount(bans),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveAllTimeUserMetrics(mongo: Db, tenantID: string) {
|
||||||
|
const [bans, total] = await Promise.all([
|
||||||
|
collection<{ count: number }>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { tenantID, "status.ban.active": true } },
|
||||||
|
{ $count: "count" },
|
||||||
|
])
|
||||||
|
.toArray(),
|
||||||
|
collection<{ count: number }>(mongo)
|
||||||
|
.aggregate([{ $match: { tenantID } }, { $count: "count" }])
|
||||||
|
.toArray(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: getCount(total),
|
||||||
|
bans: getCount(bans),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function retrieveDailyUserMetrics(
|
||||||
|
mongo: Db,
|
||||||
|
tenantID: string,
|
||||||
|
timezone: string,
|
||||||
|
now: Date
|
||||||
|
) {
|
||||||
|
const { start, end } = getTimeRange("day", timezone, now);
|
||||||
|
|
||||||
|
// Return the last 7 days (in day documents).
|
||||||
|
const results = await collection<Result>(mongo)
|
||||||
|
.aggregate([
|
||||||
|
{ $match: { tenantID, createdAt: { $gte: start, $lte: end } } },
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
$dateToString: {
|
||||||
|
date: "$createdAt",
|
||||||
|
format: getMongoFormat("day"),
|
||||||
|
timezone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
count: { $sum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return formatTimeRangeSeries("day", start, results);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Db } from "mongodb";
|
import { Db } from "mongodb";
|
||||||
|
|
||||||
import { ERROR_TYPES } from "coral-common/errors";
|
import { ERROR_TYPES } from "coral-common/errors";
|
||||||
|
|
||||||
import { Config } from "coral-server/config";
|
import { Config } from "coral-server/config";
|
||||||
import {
|
import {
|
||||||
CommentNotFoundError,
|
CommentNotFoundError,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ navigation-moderate = Moderate
|
|||||||
navigation-community = Community
|
navigation-community = Community
|
||||||
navigation-stories = Stories
|
navigation-stories = Stories
|
||||||
navigation-configure = Configure
|
navigation-configure = Configure
|
||||||
|
navigation-dashboard = Dashboard
|
||||||
|
|
||||||
## User Menu
|
## User Menu
|
||||||
userMenu-signOut = Sign Out
|
userMenu-signOut = Sign Out
|
||||||
@@ -1254,3 +1255,28 @@ hotkeysModal-shortcuts-ban = Ban comment author
|
|||||||
hotkeysModal-shortcuts-zen = Toggle single-comment view
|
hotkeysModal-shortcuts-zen = Toggle single-comment view
|
||||||
|
|
||||||
authcheck-network-error = A network error occurred. Please refresh the page.
|
authcheck-network-error = A network error occurred. Please refresh the page.
|
||||||
|
|
||||||
|
dashboard-heading-last-updated = Last updated:
|
||||||
|
|
||||||
|
dashboard-today-heading = Today's activity
|
||||||
|
dashboard-today-new-comments = New comments
|
||||||
|
dashboard-alltime-new-comments = All time total
|
||||||
|
dashboard-today-rejections = Rejection rate
|
||||||
|
dashboard-alltime-rejections = All time average
|
||||||
|
dashboard-today-staff-comments = Staff comments
|
||||||
|
dashboard-alltime-staff-comments = All time total
|
||||||
|
dashboard-today-signups = New community members
|
||||||
|
dashboard-alltime-signups = Total members
|
||||||
|
dashboard-today-bans = Banned members
|
||||||
|
dashboard-alltime-bans = Total banned members
|
||||||
|
|
||||||
|
dashboard-top-stories-today-heading = Today's most commented stories
|
||||||
|
dashboard-top-stories-table-header-story = Story
|
||||||
|
dashboard-top-stories-table-header-comments = Comments
|
||||||
|
dashboard-top-stories-no-comments = No comments today
|
||||||
|
|
||||||
|
dashboard-commenters-activity-heading = New community members this week
|
||||||
|
|
||||||
|
dashboard-comment-activity-heading = Hourly comment activity
|
||||||
|
dashboard-comment-activity-tooltip-comments = Comments
|
||||||
|
dashboard-comment-activity-legend = All-time average
|
||||||
|
|||||||
Reference in New Issue
Block a user