diff --git a/web/.env.development b/web/.env.development
new file mode 100644
index 000000000..90e649592
--- /dev/null
+++ b/web/.env.development
@@ -0,0 +1,3 @@
+NEXT_PUBLIC_ADMIN_USERNAME=admin
+NEXT_PUBLIC_ADMIN_STREAMKEY=abc123
+NEXT_PUBLIC_API_HOST=http://localhost:8080/
\ No newline at end of file
diff --git a/web/.eslintrc.js b/web/.eslintrc.js
index 2a2d06767..02acbf71d 100644
--- a/web/.eslintrc.js
+++ b/web/.eslintrc.js
@@ -23,5 +23,29 @@ module.exports = {
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": [1, { extensions: [".js", ".jsx", ".tsx"] }],
"react/jsx-props-no-spreading": "off",
+
+ 'no-unused-vars': 'off',
+ '@typescript-eslint/no-unused-vars': 'error',
+
+ 'no-use-before-define': [0],
+ '@typescript-eslint/no-use-before-define': [1],
+
+ "import/extensions": [
+ "error",
+ "ignorePackages",
+ {
+ "js": "never",
+ "jsx": "never",
+ "ts": "never",
+ "tsx": "never"
+ }
+ ]
+ },
+ settings: {
+ "import/resolver": {
+ "node": {
+ "extensions": [".js", ".jsx", ".ts", ".tsx"]
+ }
+ }
},
};
diff --git a/web/.gitignore b/web/.gitignore
index 3c3629e64..358a850a1 100644
--- a/web/.gitignore
+++ b/web/.gitignore
@@ -1 +1,4 @@
node_modules
+.env*.local
+
+.next
\ No newline at end of file
diff --git a/web/README.md b/web/README.md
index 13d24a0ab..51b274fe9 100644
--- a/web/README.md
+++ b/web/README.md
@@ -10,7 +10,7 @@ npm run dev
yarn dev
```
-Open [http://localhost:3000/admin](http://localhost:3000/admin) with your browser to see the result.
+In production this Admin instance would ideally live on the domain as your Owncast instance, for example: `myowncast-site.com/admin`. So open [http://localhost:3000/admin](http://localhost:3000/admin) with your browser to see the result.
You can start editing a page by modifying `pages/something.js`. The page auto-updates as you edit the file.
@@ -18,6 +18,15 @@ Add new pages by adding files to the `pages` directory and [routes](https://next
Since this project hits API endpoints you should make requests in [`componentDidMount`](https://reactjs.org/docs/react-component.html#componentdidmount), and not in [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching), since they're not static and we don't want to fetch them at build time, but instead at runtime.
+
+A list of API end points can be found here:
+https://github.com/owncast/owncast/blob/master/router/router.go
+
+### Auth-ing for APIs
+username: admin
+pw: [your stramkey]
+
+
## Learn More
To learn more about Next.js, take a look at the following resources:
@@ -26,3 +35,4 @@ To learn more about Next.js, take a look at the following resources:
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
diff --git a/web/favicon.ico b/web/favicon.ico
new file mode 100644
index 000000000..7a859e7ee
Binary files /dev/null and b/web/favicon.ico differ
diff --git a/web/package-lock.json b/web/package-lock.json
index 0df8f9458..fc2592dd9 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -2685,6 +2685,11 @@
"toggle-selection": "^1.0.6"
}
},
+ "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=="
+ },
"core-js-compat": {
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz",
@@ -2904,6 +2909,72 @@
"type": "^1.0.1"
}
},
+ "d3-array": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz",
+ "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw=="
+ },
+ "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": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
+ "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
+ },
+ "d3-format": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
+ "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA=="
+ },
+ "d3-interpolate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
+ "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
+ "requires": {
+ "d3-color": "1 - 2"
+ }
+ },
+ "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": "3.2.3",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.3.tgz",
+ "integrity": "sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==",
+ "requires": {
+ "d3-array": "^2.3.0",
+ "d3-format": "1 - 2",
+ "d3-interpolate": "1.2.0 - 2",
+ "d3-time": "1 - 2",
+ "d3-time-format": "2 - 3"
+ }
+ },
+ "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": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.0.0.tgz",
+ "integrity": "sha512-2mvhstTFcMvwStWd9Tj3e6CEqtOivtD8AUiHT8ido/xmzrI9ijrUUihZ6nHuf/vsScRBonagOdj0Vv+SEL5G3Q=="
+ },
+ "d3-time-format": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
+ "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
+ "requires": {
+ "d3-time": "1 - 2"
+ }
+ },
"damerau-levenshtein": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
@@ -2936,6 +3007,11 @@
"ms": "2.1.2"
}
},
+ "decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
+ },
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@@ -3041,6 +3117,14 @@
"resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.0.tgz",
"integrity": "sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA=="
},
+ "dom-helpers": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
+ "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
+ "requires": {
+ "@babel/runtime": "^7.1.2"
+ }
+ },
"dom-serializer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.0.1.tgz",
@@ -4872,11 +4956,21 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
+ "lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
+ },
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
},
+ "lodash.throttle": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+ "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
+ },
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4921,6 +5015,11 @@
"object-visit": "^1.0.0"
}
},
+ "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=="
+ },
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -6369,6 +6468,39 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
"integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg=="
},
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@@ -6482,6 +6614,108 @@
"readable-stream": "^2.0.2"
}
},
+ "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": {
+ "d3-array": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
+ "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
+ },
+ "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.5",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
+ "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ=="
+ },
+ "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-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-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.3.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz",
+ "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==",
+ "requires": {
+ "d3-time": "1"
+ }
+ }
+ }
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
"regenerate": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz",
diff --git a/web/package.json b/web/package.json
index 093050dce..834ef4456 100644
--- a/web/package.json
+++ b/web/package.json
@@ -10,13 +10,19 @@
"dependencies": {
"@ant-design/icons": "^4.2.2",
"antd": "^4.6.6",
+ "classnames": "^2.2.6",
+ "d3-scale": "^3.2.3",
+ "d3-time-format": "^3.0.0",
"next": "9.5.3",
+ "prop-types": "^15.7.2",
"react": "16.13.1",
"react-dom": "16.13.1",
+ "recharts": "^1.8.5",
"sass": "^1.26.11"
},
"devDependencies": {
"@types/node": "^14.11.2",
+ "@types/prop-types": "^15.7.3",
"@types/react": "^16.9.49",
"@typescript-eslint/eslint-plugin": "^4.3.0",
"@typescript-eslint/parser": "^4.3.0",
diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx
index d53ae791f..958b72559 100644
--- a/web/pages/_app.tsx
+++ b/web/pages/_app.tsx
@@ -1,10 +1,21 @@
-import 'antd/dist/antd.css';
-import '../styles/globals.scss'
+import 'antd/dist/antd.dark.css';
+import 'antd/dist/antd.compact.css';
+import "../styles/globals.scss";
+
+import { AppProps } from 'next/app';
+import BroadcastStatusProvider from './utils/broadcast-status-context';
+import MainLayout from './components/main-layout';
-import { AppProps } from 'next/app'
function App({ Component, pageProps }: AppProps) {
- return
+ return (
+
+
+
+
+
+
+ )
}
-export default App
\ No newline at end of file
+export default App;
\ No newline at end of file
diff --git a/web/pages/broadcast-info.tsx b/web/pages/broadcast-info.tsx
new file mode 100644
index 000000000..465b48de5
--- /dev/null
+++ b/web/pages/broadcast-info.tsx
@@ -0,0 +1,18 @@
+import React, { useContext } from 'react';
+import { BroadcastStatusContext } from './utils/broadcast-status-context';
+
+
+export default function BroadcastInfo() {
+ const context = useContext(BroadcastStatusContext);
+ const { broadcaster } = context || {};
+ const { remoteAddr, time, streamDetails } = broadcaster || {};
+
+ return (
+
+
Broadcast Info
+
Remote Address: {remoteAddr}
+
Time: {(new Date(time)).toLocaleTimeString()}
+
Stream Details: {JSON.stringify(streamDetails)}
+
+ );
+}
diff --git a/web/pages/components/logo.tsx b/web/pages/components/logo.tsx
new file mode 100644
index 000000000..a6a814185
--- /dev/null
+++ b/web/pages/components/logo.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import adminStyles from '../../styles/styles.module.css';
+
+export default function Logo() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/web/pages/components/main-layout.tsx b/web/pages/components/main-layout.tsx
new file mode 100644
index 000000000..1e099cdfc
--- /dev/null
+++ b/web/pages/components/main-layout.tsx
@@ -0,0 +1,123 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { Layout, Menu } from 'antd';
+import {
+ SettingOutlined,
+ HomeOutlined,
+ LineChartOutlined,
+ CloseCircleOutlined,
+ PlayCircleFilled,
+ StopFilled,
+ MinusSquareFilled,
+} from '@ant-design/icons';
+import classNames from 'classnames';
+
+
+import OwncastLogo from './logo';
+import { BroadcastStatusContext } from '../utils/broadcast-status-context';
+
+import adminStyles from '../../styles/styles.module.css';
+
+export default function MainLayout(props) {
+ const { children } = props;
+
+ const context = useContext(BroadcastStatusContext);
+ const { broadcastActive } = context || {};
+
+ const router = useRouter();
+ const { route } = router || {};
+
+ const { Header, Footer, Content, Sider } = Layout;
+ const { SubMenu } = Menu;
+
+ const statusIcon = broadcastActive ?
+ : ;
+ const statusMessage = broadcastActive ?
+ 'Online' : 'Offline';
+
+ const appClass = classNames({
+ 'owncast-layout': true,
+ [adminStyles.online]: broadcastActive,
+ })
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+
+MainLayout.propTypes = {
+ children: PropTypes.element.isRequired,
+};
\ No newline at end of file
diff --git a/web/pages/connected-clients.tsx b/web/pages/connected-clients.tsx
new file mode 100644
index 000000000..458e4a78e
--- /dev/null
+++ b/web/pages/connected-clients.tsx
@@ -0,0 +1,92 @@
+import React, { useState, useEffect, useContext } from 'react';
+import { Table } from 'antd';
+import { BroadcastStatusContext } from './utils/broadcast-status-context';
+
+import { CONNECTED_CLIENTS, fetchData, FETCH_INTERVAL } from './utils/apis';
+
+/*
+geo data looks like this
+ "geo": {
+ "countryCode": "US",
+ "regionName": "California",
+ "timeZone": "America/Los_Angeles"
+ }
+*/
+
+export default function ConnectedClients() {
+ const context = useContext(BroadcastStatusContext);
+ const { broadcastActive } = context || {};
+
+ const [clients, setClients] = useState([]);
+ const getInfo = async () => {
+ try {
+ const result = await fetchData(CONNECTED_CLIENTS);
+ console.log("result",result)
+ setClients(result);
+ } catch (error) {
+ console.log("==== error", error)
+ }
+ };
+
+ useEffect(() => {
+ let getStatusIntervalId = null;
+
+ getInfo();
+ if (broadcastActive) {
+ getStatusIntervalId = setInterval(getInfo, FETCH_INTERVAL);
+ // returned function will be called on component unmount
+ return () => {
+ clearInterval(getStatusIntervalId);
+ }
+ }
+ return () => [];
+ }, []);
+
+ if (!clients.length) {
+ return "no clients";
+ }
+
+ // todo - check to see if broadcast active has changed. if so, start polling.
+
+ const columns = [
+ {
+ title: 'User name',
+ dataIndex: 'username',
+ key: 'username',
+ render: username => username || '-',
+ sorter: (a, b) => a.username - b.username,
+ sortDirections: ['descend', 'ascend'],
+ },
+ {
+ title: 'Messages sent',
+ dataIndex: 'messageCount',
+ key: 'messageCount',
+ sorter: (a, b) => a.messageCount - b.messageCount,
+ sortDirections: ['descend', 'ascend'],
+ },
+ {
+ title: 'Connected Time',
+ dataIndex: 'connectedAt',
+ key: 'connectedAt',
+ render: time => (Date.now() - (new Date(time).getTime())) / 1000 / 60,
+ },
+ {
+ title: 'User Agent',
+ dataIndex: 'userAgent',
+ key: 'userAgent',
+ },
+ {
+ title: 'Location',
+ dataIndex: 'geo',
+ key: 'geo',
+ render: geo => geo && `${geo.regionName}, ${geo.countryCode}`,
+ },
+ ];
+
+ return (
+
+
Connected Clients
+
;
+
+ );
+}
diff --git a/web/pages/hardware-info.tsx b/web/pages/hardware-info.tsx
new file mode 100644
index 000000000..f26c0aeee
--- /dev/null
+++ b/web/pages/hardware-info.tsx
@@ -0,0 +1,45 @@
+import React, { useState, useEffect, useContext } from 'react';
+import { HARDWARE_STATS, fetchData, FETCH_INTERVAL } from './utils/apis';
+import { BroadcastStatusContext } from './utils/broadcast-status-context';
+
+export default function HardwareInfo() {
+ const context = useContext(BroadcastStatusContext);
+ const { broadcastActive } = context || {};
+
+ const [hardwareStatus, setHardwareStatus] = useState({});
+
+ const getHardwareStatus = async () => {
+ try {
+ const result = await fetchData(HARDWARE_STATS);
+ console.log("hardare result", result)
+
+ setHardwareStatus({ ...result });
+
+ } catch (error) {
+ setHardwareStatus({ ...hardwareStatus, message: error.message });
+ }
+ };
+
+ useEffect(() => {
+ let getStatusIntervalId = null;
+
+ getHardwareStatus();
+ getStatusIntervalId = setInterval(getHardwareStatus, FETCH_INTERVAL); //runs every 1 min.
+
+ // returned function will be called on component unmount
+ return () => {
+ clearInterval(getStatusIntervalId);
+ }
+ }, []);
+
+ return (
+
+
Hardware Info
+
cpu:[], disk: [], memory: []; value = %age.
+
the times should be the same for each, though milliseconds differ
+
+ {JSON.stringify(hardwareStatus)}
+
+
+ );
+}
diff --git a/web/pages/index.tsx b/web/pages/index.tsx
index 7284410bf..58cbdaaf9 100644
--- a/web/pages/index.tsx
+++ b/web/pages/index.tsx
@@ -1,11 +1,19 @@
+import React from 'react';
import { Card, Alert, Statistic, Row, Col } from "antd";
import { LikeOutlined } from "@ant-design/icons";
const { Meta } = Card;
-export default function Home() {
+export default function AdminHome() {
return (
+
+ < pick something
+ Home view. pretty pictures. Rainbows. Kittens.
+
+
+
+
{
+ try {
+ const result = await fetchData(SERVER_CONFIG);
+ console.log("viewers result", result)
+
+ setClients({ ...result });
+
+ } catch (error) {
+ setClients({ ...clients, message: error.message });
+ }
+ };
+
+ useEffect(() => {
+ let getStatusIntervalId = null;
+
+ getInfo();
+ getStatusIntervalId = setInterval(getInfo, FETCH_INTERVAL);
+
+ // returned function will be called on component unmount
+ return () => {
+ clearInterval(getStatusIntervalId);
+ }
+ }, []);
+
+ return (
+
+
Server Config
+
Display this data all pretty, most things will be editable in the future, not now.
+
+ {JSON.stringify(clients)}
+
+
+ );
+}
diff --git a/web/pages/utils/apis.ts b/web/pages/utils/apis.ts
new file mode 100644
index 000000000..898242ddc
--- /dev/null
+++ b/web/pages/utils/apis.ts
@@ -0,0 +1,59 @@
+/* eslint-disable prefer-destructuring */
+const ADMIN_USERNAME = process.env.NEXT_PUBLIC_ADMIN_USERNAME;
+const ADMIN_STREAMKEY = process.env.NEXT_PUBLIC_ADMIN_STREAMKEY;
+const NEXT_PUBLIC_API_HOST = process.env.NEXT_PUBLIC_API_HOST;
+
+const API_LOCATION = `${NEXT_PUBLIC_API_HOST}api/admin/`;
+
+export const FETCH_INTERVAL = 15000;
+
+// Current inbound broadcaster info
+export const BROADCASTER = `${API_LOCATION}broadcaster`;
+
+// Disconnect inbound stream
+export const DISCONNECT = `${API_LOCATION}disconnect`;
+
+// Change the current streaming key in memory
+export const STREAMKEY_CHANGE = `${API_LOCATION}changekey`;
+
+// Current server config
+export const SERVER_CONFIG = `${API_LOCATION}serverconfig`;
+
+// Get viewer count over time
+export const VIEWERS_OVER_TIME = `${API_LOCATION}viewersOverTime`;
+
+// Get currently connected clients
+export const CONNECTED_CLIENTS = `${API_LOCATION}clients`;
+
+
+// Get hardware stats
+export const HARDWARE_STATS = `${API_LOCATION}hardwarestats`;
+
+
+
+// Current Stream status (no auth)
+// use `admin/broadcaster` instead
+// export const STREAM_STATUS = '/api/status';
+
+export async function fetchData(url) {
+ const encoded = btoa(`${ADMIN_USERNAME}:${ADMIN_STREAMKEY}`);
+
+ try {
+ const response = await fetch(url, {
+ headers: {
+ 'Authorization': `Basic ${encoded}`,
+ },
+ mode: 'cors',
+ credentials: 'include',
+ });
+ if (!response.ok) {
+ const message = `An error has occured: ${response.status}`;
+ throw new Error(message);
+ }
+ const json = await response.json();
+ return json;
+ } catch (error) {
+ console.log(error)
+ }
+ return {};
+}
diff --git a/web/pages/utils/broadcast-status-context.tsx b/web/pages/utils/broadcast-status-context.tsx
new file mode 100644
index 000000000..309c2152d
--- /dev/null
+++ b/web/pages/utils/broadcast-status-context.tsx
@@ -0,0 +1,51 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+import { BROADCASTER, fetchData, FETCH_INTERVAL } from './apis';
+
+const initialState = {
+ broadcastActive: false,
+ message: '',
+ broadcaster: null,
+};
+
+export const BroadcastStatusContext = React.createContext(initialState);
+
+const BroadcastStatusProvider = ({ children }) => {
+ const [broadcasterStatus, setBroadcasterStatus] = useState(initialState);
+
+ const getBroadcastStatus = async () => {
+ try {
+ const result = await fetchData(BROADCASTER);
+ const broadcastActive = !!result.broadcaster || result.success;
+ setBroadcasterStatus({ ...result, broadcastActive });
+
+ } catch (error) {
+ setBroadcasterStatus({ ...broadcasterStatus, message: error.message });
+ }
+ };
+
+ useEffect(() => {
+ let getStatusIntervalId = null;
+
+ getBroadcastStatus();
+ getStatusIntervalId = setInterval(getBroadcastStatus, FETCH_INTERVAL);
+
+ // returned function will be called on component unmount
+ return () => {
+ clearInterval(getStatusIntervalId);
+ }
+ }, [])
+
+ return (
+
+ {children}
+
+ );
+}
+
+BroadcastStatusProvider.propTypes = {
+ children: PropTypes.element.isRequired,
+};
+
+export default BroadcastStatusProvider;
\ No newline at end of file
diff --git a/web/pages/viewer-info.tsx b/web/pages/viewer-info.tsx
new file mode 100644
index 000000000..c545506ce
--- /dev/null
+++ b/web/pages/viewer-info.tsx
@@ -0,0 +1,85 @@
+import React, { useState, useEffect, useContext } from 'react';
+import {timeFormat} from 'd3-time-format';
+import { LineChart, XAxis, YAxis, Line, Tooltip } from 'recharts';
+import { BroadcastStatusContext } from './utils/broadcast-status-context';
+
+import { VIEWERS_OVER_TIME, fetchData } from './utils/apis';
+
+const FETCH_INTERVAL = 5 * 60 * 1000; // 5 mins
+
+export default function ViewersOverTime() {
+ const context = useContext(BroadcastStatusContext);
+ const { broadcastActive } = context || {};
+
+ const [viewerInfo, setViewerInfo] = useState([]);
+
+ const getInfo = async () => {
+ try {
+ const result = await fetchData(VIEWERS_OVER_TIME);
+ setViewerInfo(result);
+ } catch (error) {
+ console.log("==== error", error)
+ }
+ };
+
+ useEffect(() => {
+ let getStatusIntervalId = null;
+
+ getInfo();
+ if (broadcastActive) {
+ getStatusIntervalId = setInterval(getInfo, FETCH_INTERVAL);
+ // returned function will be called on component unmount
+ return () => {
+ clearInterval(getStatusIntervalId);
+ }
+ }
+ return () => [];
+ }, []);
+
+
+ // todo - check to see if broadcast active has changed. if so, start polling.
+
+
+ if (!viewerInfo.length) {
+ return "no info";
+ }
+
+ const timeFormatter = (tick) => {return timeFormat('%H:%M:%S')(new Date(tick));};
+
+ const CustomizedTooltip = (props) => {
+ const { active, payload, label } = props;
+ if (active) {
+ const numViewers = payload && payload[0] && payload[0].value;
+ const time = timeFormatter(label);
+ const message = `${numViewers} viewer(s) at ${time}`;
+ return (
+
+ );
+ }
+ return null;
+ };
+
+
+ return (
+
+
Current Viewers
+
+
+
+
+ }
+ />
+
+
+
+
+ );
+}
diff --git a/web/styles/globals.scss b/web/styles/globals.scss
index e5e2dcc23..00a0ff23f 100644
--- a/web/styles/globals.scss
+++ b/web/styles/globals.scss
@@ -1,16 +1,25 @@
+$owncast-purple: rgba(90,103,216,1);;
+
html,
body {
padding: 0;
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
- Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+ font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+
+ font-size: 16px;
}
a {
color: inherit;
text-decoration: none;
+ color: rgba(90,103,216,1);
}
* {
box-sizing: border-box;
}
+
+
+.owncast-layout .ant-menu-dark.ant-menu-dark:not(.ant-menu-horizontal) .ant-menu-item-selected {
+ background-color: $owncast-purple;
+}
diff --git a/web/styles/styles.module.css b/web/styles/styles.module.css
new file mode 100644
index 000000000..5c1fe19ed
--- /dev/null
+++ b/web/styles/styles.module.css
@@ -0,0 +1,70 @@
+
+.logoSVG {
+ height: 2rem;
+ width: 2rem;
+}
+
+.owncastTitleContainer {
+ padding: 1rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+
+}
+.logoContainer {
+ background-color: #fff;
+ padding: .35rem;
+ border-radius: 9999px;
+}
+.owncastTitle {
+ display: inline-block;
+ margin-left: 1rem;
+ color: rgba(203,213,224, 1);
+ font-size: 1.15rem;
+ font-weight: 200;
+ text-transform: uppercase;
+ line-height: normal;
+ letter-spacing: .05em;
+}
+
+.contentMain {
+ padding: 3em;
+}
+
+.header {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ padding-right: 1rem;
+}
+
+.statusIndicatorContainer {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+
+}
+.statusIcon {
+ font-size: 1.5rem;
+}
+.statusIcon svg {
+ fill: #999;
+}
+.statusLabel {
+ color: #fff;
+ text-transform: uppercase;
+ font-size: .75rem;
+ display: inline-block;
+ margin-right: .5rem;
+ color: #999;
+}
+.online .statusIcon svg {
+ fill: #52c41a;
+}
+.online .statusLabel {
+ color: #52c41a;
+}
+
+/* //844-227-3943 */
\ No newline at end of file
diff --git a/web/yarn.lock b/web/yarn.lock
index d6939400c..b64975dc9 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -1020,7 +1020,7 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
-"@babel/runtime@7.11.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.8.4":
+"@babel/runtime@7.11.2", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.8.4":
version "7.11.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
@@ -1147,7 +1147,7 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==
-"@types/prop-types@*":
+"@types/prop-types@*", "@types/prop-types@^15.7.3":
version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
@@ -1747,6 +1747,11 @@ babel-plugin-transform-react-remove-prop-types@0.4.24:
resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a"
integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==
+balanced-match@^0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
+ integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=
+
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -2287,6 +2292,11 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
+core-js@^2.6.10:
+ version "2.6.11"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
+ integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
+
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -2443,6 +2453,114 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+d3-array@^1.2.0:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
+ integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
+
+d3-array@^2.3.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.8.0.tgz#f76e10ad47f1f4f75f33db5fc322eb9ffde5ef23"
+ integrity sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==
+
+d3-collection@1:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
+ integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
+
+d3-color@1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
+ integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
+
+"d3-color@1 - 2":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e"
+ integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==
+
+d3-format@1:
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
+ integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
+
+"d3-format@1 - 2":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767"
+ integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==
+
+d3-interpolate@1, d3-interpolate@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
+ integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
+ dependencies:
+ d3-color "1"
+
+"d3-interpolate@1.2.0 - 2":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163"
+ integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==
+ dependencies:
+ d3-color "1 - 2"
+
+d3-path@1:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
+ integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+
+d3-scale@^2.1.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
+ integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-scale@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.3.tgz#be380f57f1f61d4ff2e6cbb65a40593a51649cfd"
+ integrity sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g==
+ dependencies:
+ d3-array "^2.3.0"
+ d3-format "1 - 2"
+ d3-interpolate "1.2.0 - 2"
+ d3-time "1 - 2"
+ d3-time-format "2 - 3"
+
+d3-shape@^1.2.0:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
+ integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
+ integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==
+ dependencies:
+ d3-time "1"
+
+"d3-time-format@2 - 3", d3-time-format@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6"
+ integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==
+ dependencies:
+ d3-time "1 - 2"
+
+d3-time@1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
+ integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
+
+"d3-time@1 - 2":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.0.0.tgz#ad7c127d17c67bd57a4c61f3eaecb81108b1e0ab"
+ integrity sha512-2mvhstTFcMvwStWd9Tj3e6CEqtOivtD8AUiHT8ido/xmzrI9ijrUUihZ6nHuf/vsScRBonagOdj0Vv+SEL5G3Q==
+
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@@ -2487,6 +2605,11 @@ debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
dependencies:
ms "2.0.0"
+decimal.js-light@^2.4.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
+ integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==
+
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@@ -2577,6 +2700,13 @@ dom-align@^1.7.0:
resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.0.tgz#56fb7156df0b91099830364d2d48f88963f5a29c"
integrity sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA==
+dom-helpers@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
+ integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
+ dependencies:
+ "@babel/runtime" "^7.1.2"
+
dom-serializer@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.0.1.tgz#79695eb49af3cd8abc8d93a73da382deb1ca0795"
@@ -3983,12 +4113,22 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+ integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
+
lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
-lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
+lodash.throttle@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
+ integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
+
+lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@~4.17.4:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@@ -4041,6 +4181,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
+math-expression-evaluator@^1.2.14:
+ version "1.2.22"
+ resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz#c14dcb3d8b4d150e5dcea9c68c8dad80309b0d5e"
+ integrity sha512-L0j0tFVZBQQLeEjmWOvDLoRciIY8gQGWahvkztXUal8jH8R5Rlqo9GCvgqvXcy9LQhEWdQCVvzqAbxgYNt4blQ==
+
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@@ -4873,7 +5018,7 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
-prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -5352,6 +5497,36 @@ react-refresh@0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
+react-resize-detector@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c"
+ integrity sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ==
+ dependencies:
+ lodash.debounce "^4.0.8"
+ lodash.throttle "^4.1.1"
+ prop-types "^15.6.0"
+ resize-observer-polyfill "^1.5.0"
+
+react-smooth@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.5.tgz#94ae161d7951cdd893ccb7099d031d342cb762ad"
+ integrity sha512-eW057HT0lFgCKh8ilr0y2JaH2YbNcuEdFpxyg7Gf/qDKk9hqGMyXryZJ8iMGJEuKH0+wxS0ccSsBBB3W8yCn8w==
+ dependencies:
+ lodash "~4.17.4"
+ prop-types "^15.6.0"
+ raf "^3.4.0"
+ react-transition-group "^2.5.0"
+
+react-transition-group@^2.5.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
+ integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
+ dependencies:
+ dom-helpers "^3.4.0"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+ react-lifecycles-compat "^3.0.4"
+
react@16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
@@ -5416,6 +5591,46 @@ readdirp@~3.4.0:
dependencies:
picomatch "^2.2.1"
+recharts-scale@^0.4.2:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.3.tgz#040b4f638ed687a530357292ecac880578384b59"
+ integrity sha512-t8p5sccG9Blm7c1JQK/ak9O8o95WGhNXD7TXg/BW5bYbVlr6eCeRBNpgyigD4p6pSSMehC5nSvBUPj6F68rbFA==
+ dependencies:
+ decimal.js-light "^2.4.1"
+
+recharts@^1.8.5:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12"
+ integrity sha512-tM9mprJbXVEBxjM7zHsIy6Cc41oO/pVYqyAsOHLxlJrbNBuLs0PHB3iys2M+RqCF0//k8nJtZF6X6swSkWY3tg==
+ dependencies:
+ 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"
+
+reduce-css-calc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
+ integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=
+ dependencies:
+ balanced-match "^0.4.2"
+ math-expression-evaluator "^1.2.14"
+ reduce-function-call "^1.0.1"
+
+reduce-function-call@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f"
+ integrity sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==
+ dependencies:
+ balanced-match "^1.0.0"
+
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"