├── server ├── schema.js ├── resolvers │ ├── Mutation.js │ ├── Query.js │ └── Subscription.js ├── model.js ├── traqlRouter.js ├── controllers │ ├── authTokenController.js │ ├── AqlDatabaseController.js │ └── traqlController.js ├── router.js ├── server.js └── helperFuncs.js ├── README.md ├── src ├── components │ ├── HeaderBar.jsx │ ├── BottomRow │ │ ├── Log.jsx │ │ ├── BottomRow.jsx │ │ ├── AqlRowHeader.jsx │ │ ├── LogHeader.jsx │ │ ├── AqlRow.jsx │ │ ├── LogContainer.jsx │ │ ├── AqlContainer.jsx │ │ └── MutationRow.jsx │ ├── MiddleRow │ │ ├── Map.jsx │ │ ├── MapContainer.jsx │ │ ├── NodeContainer.jsx │ │ ├── PieChartContainer.jsx │ │ ├── MiddleRow.jsx │ │ ├── ErrorLogContainer.jsx │ │ ├── Node.jsx │ │ └── PieChart.jsx │ ├── Footer.jsx │ ├── TopRow │ │ ├── TopRow.jsx │ │ ├── PieChartContainer.jsx │ │ ├── LineChartContainer.jsx │ │ ├── PieChart.jsx │ │ ├── LineChart.jsx │ │ └── LineChart-old.jsx │ ├── LandingPage │ │ ├── navbar.jsx │ │ └── landingPage.jsx │ ├── useResizeObserver.js │ ├── DashboardContainer.jsx │ └── NavBar.jsx ├── index.js └── App.jsx ├── .gitignore ├── public ├── littleaql.png ├── aql-icon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── mstile-150x150.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── browserconfig.xml │ └── site.webmanifest ├── scss │ ├── application.scss │ ├── variables.scss │ ├── landingPage.scss │ ├── globals.scss │ └── dashboard.scss └── index.html ├── LICENSE ├── webpack.config.js ├── package.json └── dummydata.js /server/schema.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aql-monitor -------------------------------------------------------------------------------- /server/resolvers/Mutation.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/resolvers/Query.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/HeaderBar.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /server/resolvers/Subscription.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/littleaql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpinchak/aql-monitor/HEAD/public/littleaql.png -------------------------------------------------------------------------------- /public/aql-icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpinchak/aql-monitor/HEAD/public/aql-icon/favicon.ico -------------------------------------------------------------------------------- /public/aql-icon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpinchak/aql-monitor/HEAD/public/aql-icon/favicon-16x16.png -------------------------------------------------------------------------------- /public/aql-icon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpinchak/aql-monitor/HEAD/public/aql-icon/favicon-32x32.png -------------------------------------------------------------------------------- /public/scss/application.scss: -------------------------------------------------------------------------------- 1 | @import './globals.scss'; 2 | @import './variables.scss'; 3 | @import './dashboard.scss'; 4 | -------------------------------------------------------------------------------- /public/aql-icon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpinchak/aql-monitor/HEAD/public/aql-icon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/aql-icon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpinchak/aql-monitor/HEAD/public/aql-icon/mstile-150x150.png -------------------------------------------------------------------------------- /public/aql-icon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpinchak/aql-monitor/HEAD/public/aql-icon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/aql-icon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpinchak/aql-monitor/HEAD/public/aql-icon/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/scss/variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color: white; 2 | $secondary-color: #0fa0da; 3 | $tertiary-color: #87c5dd; 4 | $hover-color: rgb(10, 128, 175); 5 | $background-color: #1b2035; 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './App.jsx'; 4 | import '../public/scss/application.scss'; 5 | 6 | render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/components/BottomRow/Log.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | 3 | /** 4 | * Component that renders a Log Entry 5 | */ 6 | 7 | function Log() { 8 | return
; 9 | } 10 | 11 | export default Log; 12 | -------------------------------------------------------------------------------- /src/components/MiddleRow/Map.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | 3 | /** 4 | * Component that renders a Map 5 | */ 6 | 7 | function Map() { 8 | return
Map
; 9 | } 10 | 11 | export default Map; 12 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Footer() { 4 | return ( 5 |
6 | 10 |
11 | ); 12 | } 13 | 14 | export default Footer; 15 | -------------------------------------------------------------------------------- /public/aql-icon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /server/model.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const { Pool } = require('pg'); 4 | const pg_url = process.env.DATABASE_URL; 5 | const pool = new Pool({ connectionString: pg_url }); 6 | 7 | module.exports = { 8 | query: (text, params, callback) => { 9 | return pool.query(text, params, callback); 10 | }, 11 | }; -------------------------------------------------------------------------------- /server/traqlRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const db = require('./model'); 3 | const traqlRouter = express.Router(); 4 | const traqlController = require('./controllers/traqlController'); 5 | 6 | traqlRouter.post('/', traqlController.addAqlsToTraql, (req, res) => { 7 | res.sendStatus(200); 8 | }); 9 | 10 | module.exports = traqlRouter; -------------------------------------------------------------------------------- /src/components/BottomRow/BottomRow.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | // IMPORT COMPONENTS 3 | import LogContainer from './LogContainer.jsx'; 4 | 5 | function BottomRow(props) { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | export default BottomRow; 14 | -------------------------------------------------------------------------------- /src/components/MiddleRow/MapContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import Map from './Map.jsx'; 3 | 4 | function MapContainer(props) { 5 | return ( 6 |
7 |
Location Heat Map
8 | 9 |
10 | ); 11 | } 12 | 13 | export default MapContainer; 14 | -------------------------------------------------------------------------------- /src/components/MiddleRow/NodeContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import Node from './Node.jsx'; 3 | 4 | function NodeContainer(props) { 5 | return ( 6 |
7 |
Resolver Latency
8 | 9 |
10 | ); 11 | } 12 | 13 | export default NodeContainer; 14 | -------------------------------------------------------------------------------- /src/components/MiddleRow/PieChartContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import PieChart from './PieChart.jsx'; 3 | 4 | function PieChartContainer(props) { 5 | return ( 6 |
7 |
Resolver Frequency
8 | 9 |
10 | ); 11 | } 12 | 13 | export default PieChartContainer; 14 | -------------------------------------------------------------------------------- /src/components/TopRow/TopRow.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | // IMPORT COMPONENTS 3 | import LineChartContainer from './LineChartContainer.jsx'; 4 | 5 | function TopRow(props) { 6 | return ( 7 |
8 | 12 |
13 | ); 14 | } 15 | 16 | export default TopRow; 17 | -------------------------------------------------------------------------------- /server/controllers/authTokenController.js: -------------------------------------------------------------------------------- 1 | const authToken = {}; 2 | const db = require('../model.js'); 3 | 4 | authToken.getToken = (req, res, next) => { 5 | const githubId = [req.user.id]; 6 | const tokenQuery = `SELECT user_token FROM users WHERE github_id = $1;` 7 | db.query(tokenQuery, githubId) 8 | .then((token) => { 9 | res.locals.token = token.rows[0].user_token; 10 | return next() 11 | }) 12 | }; 13 | 14 | module.exports = authToken; -------------------------------------------------------------------------------- /server/router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const Router = express.Router(); 3 | const aqlDatabaseController = require('./controllers/AqlDatabaseController'); 4 | 5 | Router.get('/', aqlDatabaseController.getAqls, (req, res) => { 6 | res.status(200).json(res.locals.data); 7 | }); 8 | 9 | Router.get('/user', aqlDatabaseController.getUserData, (req, res) => { 10 | res.status(200).send(res.locals.userData); 11 | }); 12 | 13 | module.exports = Router; 14 | -------------------------------------------------------------------------------- /src/components/BottomRow/AqlRowHeader.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | 3 | /** 4 | * Component that renders a Log Entry 5 | */ 6 | 7 | function AqlRowHeader() { 8 | return ( 9 |
10 |
Time
11 |
ID
12 |
Resolver
13 |
Subscribers
14 |
Latency
15 |
16 | ); 17 | } 18 | 19 | export default AqlRowHeader; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | Aql 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/aql-icon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/BottomRow/LogHeader.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | 3 | /** 4 | * Component that renders a Log Entry 5 | */ 6 | 7 | function LogHeader() { 8 | return ( 9 |
10 |
Date
11 |
Time
12 |
ID
13 |
Resolver
14 |
Subscribers
15 |
Latency
16 |
17 |
18 | ); 19 | } 20 | 21 | export default LogHeader; 22 | -------------------------------------------------------------------------------- /src/components/MiddleRow/MiddleRow.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | // IMPORT COMPONENTS 3 | import PieChartContainer from './PieChartContainer.jsx'; 4 | import ErrorLogContainer from './ErrorLogContainer.jsx'; 5 | 6 | function MiddleRow(props) { 7 | return ( 8 |
9 | 13 | 14 |
15 | ); 16 | } 17 | 18 | export default MiddleRow; 19 | -------------------------------------------------------------------------------- /src/components/TopRow/PieChartContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import PieChart from './PieChart.jsx'; 3 | 4 | function PieChartContainer(props) { 5 | return ( 6 |
7 |
Resolver Frequency
8 | 15 |
16 | ); 17 | } 18 | 19 | export default PieChartContainer; 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/BottomRow/AqlRow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function AqlRow(props) { 4 | const localDateTime = new Date(parseInt(props.data.subscriber_received_time)); 5 | return ( 6 |
7 |
8 | {localDateTime.toLocaleTimeString()} {localDateTime.getMilliseconds()}{' '} 9 | ms 10 |
11 |
{props.data.id}
12 |
{props.data.resolver}
13 |
{props.data.expected_subscribers}
14 |
{props.data.latency}
15 |
16 | ); 17 | } 18 | 19 | export default AqlRow; 20 | -------------------------------------------------------------------------------- /src/components/BottomRow/LogContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import LogHeader from './LogHeader.jsx'; 3 | import MutationRow from './MutationRow.jsx'; 4 | 5 | function LogContainer(props) { 6 | // loop through mutations 7 | const mutationRows = []; 8 | for (let el of props.data.mutations) { 9 | mutationRows.push(); 10 | } 11 | 12 | return ( 13 |
14 | 15 |
{mutationRows}
16 |
17 | ); 18 | } 19 | 20 | export default LogContainer; 21 | -------------------------------------------------------------------------------- /src/components/TopRow/LineChartContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import LineChart from './LineChart.jsx'; 3 | 4 | function LineChartContainer(props) { 5 | 6 | return ( 7 |
8 |
Round-Trip
9 | 10 |
11 |
12 | {/**/} 16 |
17 |
18 | ); 19 | } 20 | 21 | export default LineChartContainer; 22 | -------------------------------------------------------------------------------- /src/components/BottomRow/AqlContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AqlRow from './AqlRow.jsx'; 3 | import AqlRowHeader from './AqlRowHeader.jsx'; 4 | import { animated, useSpring } from 'react-spring'; 5 | 6 | function AqlContainer(props) { 7 | const aqlContainerStyle = useSpring({ 8 | height: props.expanded ? '100px' : '0vh', 9 | opacity: props.expanded ? 1 : 0, 10 | }); 11 | 12 | const aqlRows = []; 13 | for (let el of props.data) { 14 | aqlRows.push(); 15 | } 16 | return ( 17 | 18 | 19 | {aqlRows} 20 | 21 | ); 22 | } 23 | 24 | export default AqlContainer; 25 | -------------------------------------------------------------------------------- /src/components/LandingPage/navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import { BrowserRouter as Router, Link, Switch, NavLink } from 'react-router-dom'; 3 | 4 | // const getUserData = async () => { 5 | // const response = await fetch('/githublogin'); 6 | // const data = await response.json(); 7 | // setUserData(data); 8 | // }; 9 | 10 | function Navbar () { 11 | return( 12 |
13 | 24 |
25 | ) 26 | }; 27 | 28 | export default Navbar; -------------------------------------------------------------------------------- /src/components/useResizeObserver.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import ResizeObserver from 'resize-observer-polyfill'; 3 | 4 | /** 5 | * Hook, that returns the current dimensions of an HTML element. 6 | * Doesn't play well with SVG. 7 | */ 8 | 9 | const useResizeObserver = (ref) => { 10 | const [dimensions, setDimensions] = useState(null); 11 | useEffect(() => { 12 | const observeTarget = ref.current; 13 | const resizeObserver = new ResizeObserver((entries) => { 14 | entries.forEach((entry) => { 15 | setDimensions(entry.contentRect); 16 | }); 17 | }); 18 | resizeObserver.observe(observeTarget); 19 | return () => { 20 | resizeObserver.unobserve(observeTarget); 21 | }; 22 | }, [ref]); 23 | return dimensions; 24 | }; 25 | 26 | export default useResizeObserver; 27 | -------------------------------------------------------------------------------- /src/components/MiddleRow/ErrorLogContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MutationRow from '../BottomRow/MutationRow.jsx'; 3 | import LogHeader from '../BottomRow/LogHeader.jsx'; 4 | 5 | function ErrorLogContainer(props) { 6 | if(props.length) { 7 | console.log(props) 8 | // loop through mutations 9 | const mutationRows = []; 10 | for (let el of props.data.errors) { 11 | mutationRows.push(); 12 | }; 13 | } 14 | 15 | return ( 16 |
17 | 18 |
{props.length && mutationRows.length ? mutationRows : "no errors to display"}
19 |
No data to display
20 |
21 | 22 | ); 23 | } 24 | 25 | export default ErrorLogContainer; 26 | -------------------------------------------------------------------------------- /src/components/LandingPage/landingPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Link, Switch, NavLink } from 'react-router-dom'; 3 | import Navbar from './navbar.jsx'; 4 | import aqlLogo from '../../../public/littleaql.png'; 5 | 6 | function LandingPage() { 7 | return ( 8 |
9 | 10 |
11 |
Welcome to Aql
12 |
13 | 14 |
15 |
16 | 17 | 18 | 21 | 22 | 23 |
24 |
25 |
26 | ); 27 | } 28 | 29 | export default LandingPage; -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { BrowserRouter as Router, Route, Link, Switch, NavLink } from 'react-router-dom'; 3 | 4 | // COMPONENT IMPORTS 5 | import NavBar from './components/NavBar.jsx'; 6 | import DashboardContainer from './components/DashboardContainer.jsx'; 7 | import Footer from './components/Footer.jsx'; 8 | // SCSS 9 | import '../public/scss/application.scss'; 10 | import '../public/scss/landingPage.scss'; 11 | //import landing page 12 | import LandingPage from '../src/components/LandingPage/landingPage.jsx'; 13 | import Cookies from 'js-cookie'; 14 | 15 | const userTokenCookie = Cookies.get('userToken'); 16 | 17 | function App() { 18 | const [userToken, setUserToken] = useState(userTokenCookie); 19 | return ( 20 | userToken ? 21 |
22 | 23 | 24 |
25 | : 26 | 27 | 28 | 29 | ) 30 | }; 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 AqlOrg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: process.env.NODE_ENV, 5 | entry: './src/index.js', 6 | output: { 7 | filename: 'bundle.js', 8 | path: path.resolve(__dirname, './build'), 9 | publicPath: '/build/', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$|jsx/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/preset-env', '@babel/preset-react'], 19 | }, 20 | }, 21 | exclude: /node_modules/, 22 | }, 23 | { 24 | test: /\.s?css$/, 25 | use: ['style-loader', 'css-loader', 'sass-loader'], 26 | }, 27 | { 28 | test: /\.(png|jpe?g|gif)$/i, 29 | use: { 30 | loader: 'file-loader', 31 | options: { 32 | name: '[path][name].[ext]', 33 | }, 34 | }, 35 | }, 36 | ], 37 | }, 38 | resolve: { 39 | extensions: ['.js', '.jsx'], 40 | }, 41 | devServer: { 42 | contentBase: path.resolve(__dirname, './public'), 43 | port: 8080, 44 | proxy: { 45 | '/': 'http://localhost:3000', 46 | }, 47 | publicPath: '/build/', 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/DashboardContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | // IMPORT COMPONENTS 3 | import TopRow from './TopRow/TopRow.jsx'; 4 | import MiddleRow from './MiddleRow/MiddleRow.jsx'; 5 | import BottomRow from './BottomRow/BottomRow.jsx'; 6 | import Cookies from 'js-cookie'; 7 | 8 | const userTokenCookie = Cookies.get('userToken'); 9 | 10 | function DashboardContainer(props) { 11 | 12 | const [ready, setReady] = useState(false); 13 | const [aqlData, setAqlData] = useState({}); 14 | const [userToken, setUserToken] = useState(userTokenCookie); 15 | const [userInfo, setUserInfo] = useState({}) 16 | 17 | // fetching user data 18 | useEffect(() => { 19 | fetch('/api/user') 20 | // .then(res => res.json()) 21 | .then(res => setUserInfo(res)) 22 | .catch(err => console.log(err)); 23 | }, [userToken]); 24 | 25 | // fetching user analytics 26 | useEffect(() => { 27 | fetch('/api') 28 | .then(res => res.json()) 29 | .then(data => setAqlData(data)) 30 | .then(() => setReady(true)) 31 | .catch(err => console.log(err)); 32 | }, []); 33 | 34 | return ( 35 | ready && ( 36 |
37 | 40 | 43 | 44 |
45 | ) 46 | ); 47 | } 48 | 49 | 50 | export default DashboardContainer; 51 | -------------------------------------------------------------------------------- /src/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import Cookies from 'js-cookie'; 3 | 4 | function logout() { 5 | Cookies.remove('userToken', { path: ''}) 6 | window.location.reload(false) 7 | } 8 | 9 | function NavBar() { 10 | const dropdownRef= useRef(null); 11 | const [isActive, setIsActive] = useState(false); 12 | const handleDropdown = () => setIsActive(!isActive); 13 | 14 | // collapse dropdown menu when clicking away 15 | useEffect(() => { 16 | const pageClickEvent = (e) => { 17 | // If the active element exists and is clicked outside of 18 | if (dropdownRef.current !== null && !dropdownRef.current.contains(e.target)) { 19 | setIsActive(!isActive); 20 | } 21 | }; 22 | 23 | // if menu is active, then listen for clicks 24 | if(isActive) { 25 | window.addEventListener('click', pageClickEvent); 26 | } 27 | 28 | return () => { 29 | window.removeEventListener('click', pageClickEvent); 30 | } 31 | 32 | }, [isActive]); 33 | 34 | return ( 35 | 46 | ); 47 | } 48 | 49 | export default NavBar; 50 | -------------------------------------------------------------------------------- /public/scss/landingPage.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | #nav { 4 | width: 100%; 5 | float: left; 6 | margin: 0 0 3em 0; 7 | padding: 0; 8 | list-style: none; 9 | font-family: Arial, Helvetica, sans-serif; 10 | 11 | li { 12 | float: left; 13 | 14 | a { 15 | display: block; 16 | padding: 15px 15px; 17 | text-decoration: none; 18 | font-size: 14px; 19 | color: white; 20 | } 21 | } 22 | } 23 | 24 | #landingdiv { 25 | position: absolute; 26 | width: 30vw; 27 | height: 30vw; 28 | background-color: none; 29 | display: flex; 30 | flex-direction: column; 31 | top: 40%; 32 | left: 50%; 33 | margin-top: -15vw; 34 | margin-left: -15vw; 35 | justify-content: space-evenly; 36 | align-items: center; 37 | } 38 | 39 | #welcome { 40 | font-family: Arial, Helvetica, sans-serif; 41 | color: $secondary-color; 42 | font-size: 2vw; 43 | } 44 | 45 | #aqlholder { 46 | margin-top: 5px; 47 | display: flex; 48 | background-color: $primary-color; 49 | border: none; 50 | border-radius: 25%; 51 | width: 20vw; 52 | height: 20vw; 53 | justify-content: center; 54 | align-items: center; 55 | } 56 | 57 | #landingAql { 58 | position: absolute; 59 | width: 50%; 60 | } 61 | 62 | .buttonholder { 63 | width: 105px; 64 | } 65 | 66 | #loginbutton { 67 | background-color: $secondary-color; 68 | color: $primary-color; 69 | margin: 1em 1em 0.5em 0; 70 | border-radius: 5px; 71 | padding: 5px 15px 5px 15px; 72 | border: none; 73 | outline: none; 74 | font-size: 13px; 75 | transition: 0.5s; 76 | } 77 | 78 | #loginbutton:hover { 79 | background-color: $hover-color; 80 | } -------------------------------------------------------------------------------- /src/components/BottomRow/MutationRow.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import AqlContainer from './AqlContainer.jsx'; 3 | import { CgAdd, CgCloseO } from 'react-icons/cg'; 4 | import { useSpring, animated, interpolate } from 'react-spring'; 5 | 6 | function MutationRow(props) { 7 | const [expanded, setExpanded] = useState(false); 8 | const handleClick = () => { 9 | setExpanded(!expanded); 10 | }; 11 | const dateTime = new Date(parseInt(props.data.dateTime)); 12 | 13 | const spring = useSpring({ 14 | x: expanded ? 45 : 0, 15 | y: expanded ? 1 : 0, 16 | config: { tension: 800 }, 17 | }); 18 | 19 | const AnimatedIcon = animated(CgAdd); 20 | 21 | return ( 22 |
31 |
32 |
{dateTime.toLocaleDateString()}
33 |
{dateTime.toLocaleTimeString()}
34 |
{props.data.mutationId}
35 |
{props.data.resolver}
36 |
{props.data.expectedAqls}
37 |
{props.data.avgLatency}
38 | 49 |
50 | 51 |
52 | ); 53 | } 54 | 55 | export default MutationRow; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aql-monitor", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node ./server/server.js", 9 | "build": "cross-env NODE_ENV=producton webpack", 10 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open\" \"nodemon ./server/server.js\"" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/AqlOrg/aql-monitor.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/AqlOrg/aql-monitor/issues" 20 | }, 21 | "homepage": "https://github.com/AqlOrg/aql-monitor#readme", 22 | "dependencies": { 23 | "chart.js": "^2.9.3", 24 | "axios": "^0.20.0", 25 | "d3": "^6.2.0", 26 | "express": "^4.17.1", 27 | "graphql": "^15.3.0", 28 | "js-cookie": "^2.2.1", 29 | "passport": "^0.4.1", 30 | "passport-github": "^1.1.0", 31 | "pg": "^8.4.0", 32 | "react": "^16.13.1", 33 | "react-chartjs-2": "^2.10.0", 34 | "react-dom": "^16.13.1", 35 | "react-icons": "^3.11.0", 36 | "react-spring": "^8.0.27", 37 | "react-router-dom": "^5.2.0", 38 | "resize-observer-polyfill": "^1.5.1", 39 | "sass": "^1.26.11", 40 | "uuid": "^8.3.1" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.11.6", 44 | "@babel/preset-env": "^7.11.5", 45 | "@babel/preset-react": "^7.10.4", 46 | "babel-core": "^6.26.3", 47 | "babel-loader": "^8.1.0", 48 | "babel-polyfill": "^6.26.0", 49 | "babel-preset-es2015": "^6.24.1", 50 | "babel-preset-stage-0": "^6.24.1", 51 | "concurrently": "^5.3.0", 52 | "cors": "^2.8.5", 53 | "cross-env": "^7.0.2", 54 | "css-loader": "^4.3.0", 55 | "dotenv": "^8.2.0", 56 | "file-loader": "^6.1.1", 57 | "nodemon": "^2.0.4", 58 | "sass-loader": "^10.0.2", 59 | "style-loader": "^1.3.0", 60 | "webpack": "^4.44.2", 61 | "webpack-cli": "^3.3.12", 62 | "webpack-dev-server": "^3.11.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /server/controllers/AqlDatabaseController.js: -------------------------------------------------------------------------------- 1 | const aqlDatabaseController = {}; 2 | const db = require('../model'); 3 | const { 4 | resolverStats, 5 | subscriptionHistory, 6 | mutations, 7 | } = require('../helperFuncs'); 8 | const { query } = require('express'); 9 | 10 | //Analytics from Aql table 11 | aqlDatabaseController.getAqls = (req, res, next) => { 12 | const userToken = req.headers.cookie; 13 | //use the mainUserToken to query the database 14 | const mainUserToken = [...userToken].splice(10,[...userToken].length - 1).join(''); 15 | 16 | const tokenQuery = [mainUserToken]; 17 | const queryString = `SELECT * FROM aql WHERE user_token = $1;`; 18 | 19 | db.query(queryString, tokenQuery, (err, data) => { 20 | // If error, console.log 21 | if (err) console.log('ERROR: ', err); 22 | // create a dataObj to add shaped data from calling helperFuncs 23 | const dataObj = {}; 24 | if(data.rows.length > 0) { 25 | dataObj.resolverStats = resolverStats(data); 26 | dataObj.subscriptionHistory = subscriptionHistory(data); 27 | // destructure mutations and errors from E.R. of mutations(data) 28 | const [mutationsArr, errors] = mutations(data); 29 | // add mutations prop with mutations as val 30 | // add errors prop 31 | dataObj.mutations = mutationsArr; 32 | dataObj.errors = errors; 33 | res.locals.data = dataObj; 34 | } 35 | return next(); 36 | }); 37 | } 38 | 39 | //Querying user data from the database by user_token 40 | aqlDatabaseController.getUserData = (req, res, next) => { 41 | const userToken = req.headers.cookie; 42 | const mainUserToken = [...userToken].splice(10,[...userToken].length - 1).join(''); 43 | 44 | const queryString = ` 45 | SELECT 46 | username, 47 | github_id, 48 | avatar_url 49 | FROM users 50 | WHERE user_token = $1; 51 | ` 52 | const tokenQuery = [mainUserToken]; 53 | db.query(queryString, tokenQuery) 54 | .then(userData => { 55 | res.locals.userData = userData.rows[0]; 56 | return next(); 57 | }) 58 | } 59 | 60 | module.exports = aqlDatabaseController; 61 | -------------------------------------------------------------------------------- /src/components/TopRow/PieChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import * as d3 from 'd3'; 3 | 4 | const PieChart = (props) => { 5 | 6 | let uselessData = [{name:'fds', value: 100}, {name:'fds', value: 150}]; 7 | const moreuseless = [1,2,3,4] 8 | //let dumData = props.dummyData.resolverStats.map(elt => elt); 9 | //console.log(dumData); 10 | 11 | const ref = useRef(); 12 | 13 | const createPie = d3 14 | .pie() 15 | .value((d) => d.value) 16 | .sort(null); 17 | const createArc = d3 18 | .arc() 19 | .innerRadius(props.innerRadius) 20 | .outerRadius(props.outerRadius); 21 | const colors = d3.interpolateBlues; 22 | 23 | // useEffect(() => { 24 | 25 | // const data = createPie(dumData); 26 | 27 | // const group = d3.select(ref.current); 28 | // const groupWithData = group.selectAll('g.arc').data(data); 29 | 30 | // groupWithData 31 | // .enter() 32 | // .append('g') 33 | // .attr('class', 'arc'); 34 | 35 | // const path = groupWithData 36 | // .append('path') 37 | // .merge(groupWithData.select('path.arc')); 38 | 39 | // path 40 | // .attr('class', 'arc') 41 | // .attr('d', createArc) 42 | // .attr('fill', (d, i) => colors(i/(props.resolverStats.length))); 43 | 44 | // const text = groupWithData 45 | // .append('text') 46 | // .merge(groupWithData.select('text')); 47 | 48 | // text 49 | // .attr('text-anchor', 'middle') 50 | // .attr('alignment-baseline', 'middle') 51 | // .attr('transform', (d) => `translate(${createArc.centroid(d)})`) 52 | // .style('fill', 'black') 53 | // .style('font-size', '10') 54 | // .style('font-family', 'Arial') 55 | // .style('text-align', 'middle') 56 | // //.text((d) => d.data.name); 57 | // }, []); 58 | 59 | return ( 60 | props.length? 61 | 62 | 66 | 67 | : 68 |
No data to display
69 | ); 70 | }; 71 | 72 | export default PieChart; 73 | -------------------------------------------------------------------------------- /public/scss/globals.scss: -------------------------------------------------------------------------------- 1 | @import './variables'; 2 | 3 | html, 4 | body { 5 | background-color: $background-color; 6 | } 7 | 8 | // NAVBAR STYLES: LOGO AND USER BUTTON 9 | #navbar { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | position: fixed; 14 | top: 0px; 15 | width: 100vw; 16 | padding-top: 15px; 17 | padding-bottom: 15px; 18 | // border-bottom: 1px solid $secondary-color; 19 | 20 | font-family: Georgia, 'Times New Roman', Times, serif; 21 | 22 | #logo { 23 | font-size: 40px; 24 | color: $primary-color; 25 | margin-left: 25px; 26 | } 27 | 28 | #logoutbutton { 29 | position: absolute; 30 | transform: translate(-45px, -10px); 31 | color: $primary-color; 32 | background-color: $background-color; 33 | border: none; 34 | border: none; 35 | outline: none; 36 | } 37 | 38 | #userbutton { 39 | border-radius: 50%; 40 | background-color: $secondary-color; 41 | border: none; 42 | height: 30px; 43 | width: 30px; 44 | margin-right: 25px; 45 | border: none; 46 | outline: none; 47 | z-index: 10; 48 | transition: 0.5s; 49 | } 50 | 51 | #userbutton:hover { 52 | background-color: $hover-color; 53 | } 54 | 55 | .dropdown-menu { 56 | background-color: $background-color; 57 | border: 1px solid $secondary-color; 58 | border-radius: 5px; 59 | position: absolute; 60 | top: 60px; 61 | right: 25px; 62 | width: 85px; 63 | height: 30px; 64 | opacity: 0; 65 | visibility: hidden; 66 | transform: translateY(-20px); 67 | display: flex; 68 | flex-direction: column; 69 | vertical-align: middle; 70 | align-items: center; 71 | transition: 0.5s ease; 72 | } 73 | 74 | .dropdown-menu.active { 75 | opacity: 1; 76 | visibility: visible; 77 | transform: translateY(0); 78 | top: 60px; 79 | } 80 | 81 | .dropdown-menu:hover { 82 | border: 1px solid $tertiary-color; 83 | } 84 | 85 | .dropdown-menu ul { 86 | list-style: none; 87 | } 88 | 89 | .menu-trigger { 90 | background-color: $background-color; 91 | color: $primary-color; 92 | transition: 0.5s; 93 | } 94 | 95 | .menu-trigger:hover { 96 | background-color: #272d47; 97 | } 98 | 99 | } 100 | 101 | // FOOTER STYLES 102 | -------------------------------------------------------------------------------- /src/components/MiddleRow/Node.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | import { select, forceSimulation, forceLink, forceManyBody, forceCenter } from 'd3'; 3 | 4 | function Node() { 5 | 6 | const svgRef = useRef(); 7 | 8 | // const [newData, setNewData] = useState({ 9 | // nodes: [ 10 | // {"name": "Caql"}, 11 | // {"name": "Aql"}, 12 | // {"name": "Raql"}, 13 | // {"name": "Jaql"}, 14 | // {"name": "Maql"}, 15 | // {"name": "Craql"} 16 | // ], 17 | // links: [ 18 | // {"src": "Caql", "trg": "Jaql"}, 19 | // {"src": "Craql", "trg": "Maql"}, 20 | // {"src": "Maql", "trg": "Jaql"}, 21 | // {"src": "Raql", "trg": "Caql"}, 22 | // {"src": "Caql", "trg": "Aql"} 23 | // ] 24 | // }); 25 | 26 | const [newNodes, setNewNodes] = useState([ 27 | {"name": "Caql"}, 28 | {"name": "Aql"}, 29 | {"name": "Raql"}, 30 | {"name": "Jaql"}, 31 | {"name": "Maql"}, 32 | {"name": "Craql"} 33 | ]); 34 | 35 | const [newLinks, setNewLinks] = useState([ 36 | {"source": "Aql", "target": "Caql"}, 37 | {"source": "Aql", "target": "Jaql"}, 38 | {"source": "Aql", "target": "Maql"}, 39 | {"source": "Aql", "target": "Raql"}, 40 | {"source": "Aql", "target": "Craql"} 41 | ]); 42 | 43 | useEffect(() => { 44 | 45 | const svg = select(svgRef.current); 46 | 47 | let link = svg.selectAll("line") 48 | .data(newLinks) 49 | .enter() 50 | .append("line") 51 | .attr("stroke-width", 1) 52 | .style("stroke", "white"); 53 | 54 | let node = svg.selectAll("circle") 55 | .data(newNodes) 56 | .enter() 57 | .append("circle") 58 | .attr("r", 5) 59 | .attr("fill", "teal"); 60 | 61 | let simulation = forceSimulation() 62 | .force("link", forceLink().id((d) => d.name)) 63 | .force("charge", forceManyBody()) 64 | .force("center", forceCenter(100, 100)); 65 | 66 | simulation 67 | .nodes(newNodes) 68 | .on("tick", ticked); 69 | 70 | simulation.force("link").links(newLinks); 71 | 72 | function ticked() { 73 | link 74 | .attr("x1", (d) => d.source.x) 75 | .attr("y1", (d) => d.source.y) 76 | .attr("x2", (d) => d.target.x) 77 | .attr("y2", (d) => d.target.y); 78 | 79 | node 80 | .attr("cx", (d) => d.x) 81 | .attr("cy", (d) => d.y); 82 | } 83 | 84 | }, [newNodes, newLinks]); 85 | 86 | 87 | 88 | return ( 89 |
90 | 91 |
92 | ); 93 | } 94 | 95 | export default Node; 96 | -------------------------------------------------------------------------------- /server/controllers/traqlController.js: -------------------------------------------------------------------------------- 1 | const db = require('../model.js'); 2 | 3 | const traqlController = {}; 4 | 5 | traqlController.addAqlsToTraql = (req, res, next) => { 6 | let receivedTraqlEntries = req.body; 7 | let queryString = ''; 8 | // create query string to send Aqls to the database 9 | if(receivedTraqlEntries.successAqls.length) { 10 | // create base of query string to send all values to db 11 | queryString += `insert into Aql (id, mutation_send_time, mutation_received_time, subscriber_received_time, latency, mutation_id, resolver, expected_subscribers, successful_subscribers, user_token) values `; 12 | for(let traql of receivedTraqlEntries.successAqls) { 13 | // loop through aqls in mutation Id 14 | for (let aql of traql.aqlsReceivedBack) { 15 | // add one aql of data to query string 16 | queryString += `('${aql.id}', '${aql.mutationSendTime}', '${aql.mutationReceived}', '${aql.subscriberReceived}', '${aql.roundtripTime}', '${aql.mutationId}', '${aql.resolver}', '${traql.expectedNumberOfAqls}', '${traql.aqlsReceivedBack.length}', '${aql.userToken}'),`; 17 | } 18 | } 19 | // format the query to remove final comma and add semicolon at end 20 | queryString = queryString.slice(0, -1) + ';'; 21 | } 22 | if(receivedTraqlEntries.errorAqls.length) { 23 | for(let traql of receivedTraqlEntries.errorAqls) { 24 | if(traql.aqlsReceivedBack.length) { 25 | queryString += `insert into Aql (id, mutation_send_time, mutation_received_time, subscriber_received_time, latency, mutation_id, resolver, expected_subscribers, successful_subscribers, error, user_token) values `; 26 | for (let aql of traql.aqlsReceivedBack) { 27 | // add each aql to the query string 28 | queryString += `('${aql.id}', '${aql.mutationSendTime}', '${aql.mutationReceived}', '${aql.subscriberReceived}', '${aql.roundtripTime}', '${aql.mutationId}', '${aql.resolver}', ${traql.expectedNumberOfAqls}, ${traql.aqlsReceivedBack.length}, 'true', '${aql.userToken}'),`; 29 | } 30 | // format the query to remove final comma and add semicolon at end 31 | queryString = queryString.slice(0, -1) + ';'; 32 | } 33 | // create error row for db with mutationID and traql stats 34 | queryString += `insert into Aql (mutation_id, mutation_received_time, resolver, expected_subscribers, successful_subscribers, error, user_token) values ('${traql.mutationId}', '${traql.openedTime}', '${traql.resolver}', '${traql.expectedNumberOfAqls}', '${traql.aqlsReceivedBack.length}', 'true', '${traql.userToken}');`; 35 | } 36 | } 37 | console.log('query string'); 38 | console.log(queryString); 39 | db.query(queryString, (err, response) => { 40 | if (err) { 41 | return next(err); 42 | } 43 | console.log('Data successfully added to db'); 44 | return next(); 45 | }); 46 | return next(); 47 | }; 48 | 49 | module.exports = traqlController; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const GitHubStrategy = require('passport-github').Strategy; 4 | const passport = require('passport'); 5 | const app = express(); 6 | const PORT = 3000; 7 | const db = require('./model.js'); 8 | const { v4: uuidv4 } = require('uuid'); 9 | const authToken = require('./controllers/authTokenController.js'); 10 | const Cookies = require ('js-cookie'); 11 | 12 | const router = require('./router'); 13 | const traqlRouter = require('./traqlRouter'); 14 | 15 | app.use(express.json()); 16 | app.use(express.urlencoded({ extended: true })); 17 | app.use(cors({ origin: ['http://localhost:8080', 'http://localhost:3000'] })); 18 | app.use(express.static('public')); 19 | 20 | app.use('/aqls', traqlRouter); 21 | app.use('/api', router); 22 | 23 | // ---------------------- GitHub OAuth Section ----------------------- // 24 | 25 | // Initialize Passport 26 | app.use(passport.initialize()); 27 | 28 | // Incorporating GitHub strategy with environment variables 29 | passport.use(new GitHubStrategy({ 30 | clientID: process.env.GITHUB_CLIENT_ID, // process.env.GITHUB_CLIENT_ID 31 | clientSecret: process.env.GITHUB_CLIENT_SECRET, // process.env.GITHUB_CLIENT_SECRET 32 | callbackURL: 'http://localhost:8080/auth/github/callback', // callback URL from GitHub 33 | }, 34 | // Async callback function: params- tokens, GitHub profile, callback 35 | async (accessToken, refreshToken, profile, cb) => { 36 | // find profile in users table based on githubId 37 | const verifyUser = `SELECT * FROM users WHERE github_id = $1;` 38 | const githubId = [profile.id]; 39 | const aqlsUser = await db.query(verifyUser, githubId); 40 | 41 | //insert this data to user table 42 | const signupQuery = ` 43 | INSERT into users ( 44 | username, 45 | display_name, 46 | github_id, 47 | avatar_url, 48 | user_token 49 | ) 50 | VALUES ($1, $2, $3, $4, $5); 51 | ` 52 | const singupArray = [ 53 | profile.username, 54 | profile.displayName, 55 | profile.id, 56 | profile._json.avatar_url, 57 | uuidv4() 58 | ] 59 | 60 | //if aqlsUser does not exist in the database insert user data into user table 61 | if (!aqlsUser.rows.length) { 62 | db.query(signupQuery, singupArray) 63 | } 64 | cb(null, profile); 65 | } 66 | )); 67 | 68 | // Setting up Express routing for POST request to login via OAuth 69 | app.get('/githublogin', passport.authenticate('github', { session: false })); 70 | 71 | // Callback with GitHub OAuth to eventually redirect users to dashboard 72 | app.get( 73 | '/auth/github/callback', 74 | passport.authenticate('github', { session: false }), 75 | authToken.getToken, 76 | (req, res) => { 77 | res.locals.username = req.user.username; 78 | res.locals.id = req.user.id; 79 | res.locals.avatar = req.user._json.avatar_url; 80 | res.cookie('userToken', res.locals.token); 81 | res.redirect('/'); 82 | } 83 | ); 84 | 85 | module.exports = app.listen(PORT, () => { 86 | console.log('Aql hears you loud and clear on port 3000'); 87 | }); 88 | 89 | -------------------------------------------------------------------------------- /server/helperFuncs.js: -------------------------------------------------------------------------------- 1 | //return obj with resolver names as properties and count as value 2 | function resolverStats(data) { 3 | // create returnObj 4 | const returnObj = {}; 5 | // for element of data.rows 6 | for (let el of data.rows) { 7 | // if returnObj has key of element.resolver 8 | if (returnObj[el.resolver]) { 9 | // increment value 10 | returnObj[el.resolver] += 1; 11 | } else { 12 | // else make new key of value 1 13 | returnObj[el.resolver] = 1; 14 | } 15 | } 16 | const returnArr = []; 17 | for (let key in returnObj) { 18 | const resolverObj = {}; 19 | resolverObj.name = key; 20 | resolverObj.value = returnObj[key]; 21 | returnArr.push(resolverObj); 22 | } 23 | return returnArr; 24 | } 25 | 26 | // return object that represent the num of subs at every point in time where there was a mutation 27 | function subscriptionHistory(data) { 28 | // create returnObject 29 | const returnObj = {}; 30 | // iterate through data 31 | for (let el of data.rows) { 32 | // if returnObj doesnt have key of mutationReceivedTime 33 | if (!returnObj[el.mutation_received_time]) { 34 | //create that key and set it to expected_subscribers 35 | returnObj[el.mutation_received_time] = el.expected_subscribers; 36 | } 37 | } 38 | // return returnObj 39 | return returnObj; 40 | } 41 | 42 | function mutations(data) { 43 | //create hashes to store aqls and errorAqls 44 | const hashql = {}; 45 | let returnArr = []; 46 | const errorql = {}; 47 | let errorArr = []; 48 | 49 | // getAvg function returns average latency of array of objects with latency property 50 | function getAvg(array) { 51 | let total = 0; 52 | if (array.length) { 53 | for (let el of array) { 54 | total += parseInt(el.latency); 55 | } 56 | total = total / array.length; 57 | } 58 | return Math.round(total); 59 | } 60 | 61 | for (let el of data.rows) { 62 | if (!el.error) { 63 | if (!hashql[el.mutation_id]) { 64 | hashql[el.mutation_id] = [el]; 65 | } else { 66 | hashql[el.mutation_id].push(el); 67 | } 68 | } else { 69 | // return array of mutations that were errors, including any aqls that came back 70 | if (!errorql[el.mutation_id]) { 71 | // build error Mutation obj from shared data 72 | const errorMutObj = {}; 73 | errorMutObj.mutationId = el.mutation_id; 74 | errorMutObj.resolver = el.resolver; 75 | errorMutObj.dateTime = el.mutation_received_time; 76 | errorMutObj.expected = el.expected_subscribers; 77 | errorMutObj.received = el.successful_subscribers; 78 | errorMutObj.aqls = el.id ? [el] : []; 79 | errorMutObj.error = true; 80 | errorql[el.mutation_id] = errorMutObj; 81 | } else { 82 | if (el.id) { 83 | errorql[el.mutation_id].aqls.push(el); 84 | } 85 | } 86 | } 87 | } 88 | // loop through hash and create properties on mutation object 89 | for (let key in hashql) { 90 | const mutObj = {}; 91 | mutObj.mutationId = key; 92 | mutObj.resolver = hashql[key][0].resolver; 93 | mutObj.expectedAqls = hashql[key][0].expected_subscribers; 94 | mutObj.dateTime = hashql[key][0].mutation_received_time; 95 | mutObj.aqls = hashql[key]; 96 | mutObj.avgLatency = getAvg(mutObj.aqls); 97 | returnArr.push(mutObj); 98 | } 99 | //calculate average latency of items in errorql 100 | for (let key in errorql) { 101 | errorql[key].avgLatency = getAvg(errorql[key].aqls); 102 | errorArr.push(errorql[key]); 103 | } 104 | 105 | //sort return arrays 106 | returnArr = returnArr.sort((a, b) => (a.dateTime > b.dateTime ? 1 : -1)); 107 | errorArr = errorArr.sort((a, b) => (a.dateTime > b.dateTime ? 1 : -1)); 108 | return [returnArr, errorArr]; 109 | } 110 | 111 | module.exports = { 112 | resolverStats, 113 | subscriptionHistory, 114 | mutations, 115 | }; 116 | -------------------------------------------------------------------------------- /src/components/MiddleRow/PieChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | // import * as d3 from 'd3'; 3 | import { Pie } from 'react-chartjs-2'; 4 | 5 | const PieChart = (props) => { 6 | const state = { 7 | labels: props.resolverStats.map((elt) => elt.name), 8 | datasets: [ 9 | { 10 | label: 'Resolvers', 11 | backgroundColor: ['#1167b1', '#40e0d0', '#187bcd', '#2a9df4'], 12 | hoverBackgroundColor: ['#187bcd', '#2a9df4', '#1167b1', '#d0efff'], 13 | data: props.resolverStats.map((elt) => elt.value), 14 | borderWidth: 0, 15 | }, 16 | ], 17 | }; 18 | 19 | const options = { 20 | legend: { 21 | display: true, 22 | position: 'bottom', 23 | padding: 0, 24 | align: 'center', 25 | labels: { 26 | boxWidth: 10, 27 | fontColor: 'lightgrey', 28 | }, 29 | }, 30 | }; 31 | return ( 32 |
33 |
34 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default PieChart; 41 | 42 | // const Slice = (props) => { 43 | // let { pie } = props; 44 | // let arc = d3.arc().innerRadius(0).outerRadius(100); 45 | 46 | // let interpolate = d3.interpolateRgb('#eaaf79', '#bc3358'); 47 | 48 | // return pie.map((slice, index) => { 49 | // let sliceColor = interpolate(index / (pie.length - 1)); 50 | 51 | // return ; 52 | // }); 53 | // }; 54 | 55 | // export const PieChart = (props) => { 56 | // console.log('props.resolverStats:', props.resolverStats); 57 | // let data = props.resolverStats.map((elt) => elt.value); 58 | // let pie = d3.pie()(data); 59 | 60 | // return ( 61 | // 62 | // 63 | // 64 | // 65 | // 66 | // ); 67 | // }; 68 | 69 | //-----------------FIRST-----------------// 70 | 71 | // const PieChart = (props) => { 72 | 73 | // let uselessData = [{name:'fds', value: 100}, {name:'fds', value: 150}]; 74 | // const moreuseless = [1,2,3,4] 75 | // let dumData = props.dummyData.resolverStats.map(elt => elt); 76 | // console.log(dumData); 77 | 78 | // const ref = useRef(); 79 | 80 | // const createPie = d3 81 | // .pie() 82 | // .value((d) => d.value) 83 | // .sort(null); 84 | // const createArc = d3 85 | // .arc() 86 | // .innerRadius(props.innerRadius) 87 | // .outerRadius(props.outerRadius); 88 | // const colors = d3.interpolateBlues; 89 | 90 | // useEffect(() => { 91 | 92 | // const data = createPie(dumData); 93 | 94 | // const group = d3.select(ref.current); 95 | // const groupWithData = group.selectAll('g.arc').data(data); 96 | 97 | // groupWithData 98 | // .enter() 99 | // .append('g') 100 | // .attr('class', 'arc'); 101 | 102 | // const path = groupWithData 103 | // .append('path') 104 | // .merge(groupWithData.select('path.arc')); 105 | 106 | // path 107 | // .attr('class', 'arc') 108 | // .attr('d', createArc) 109 | // .attr('fill', (d, i) => colors(i/(props.resolverStats.length))); 110 | 111 | // const text = groupWithData 112 | // .append('text') 113 | // .merge(groupWithData.select('text')); 114 | 115 | // text 116 | // .attr('text-anchor', 'middle') 117 | // .attr('alignment-baseline', 'middle') 118 | // .attr('transform', (d) => `translate(${createArc.centroid(d)})`) 119 | // .style('fill', 'black') 120 | // .style('font-size', '10') 121 | // .style('font-family', 'Arial') 122 | // .style('text-align', 'middle') 123 | // //.text((d) => d.data.name); 124 | // }, []); 125 | 126 | // return ( 127 | // 128 | // 132 | // 133 | // ); 134 | // }; 135 | -------------------------------------------------------------------------------- /src/components/TopRow/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import moment from 'moment'; 4 | 5 | function LineChart(props) { 6 | let [avgLatency, setAvgLatency] = useState( 7 | props.mutationData.map((elt) => parseInt(elt.avgLatency)) 8 | ); 9 | let [avgSubscribers, setAvgSubscribers] = useState( 10 | props.mutationData.map((elt) => parseInt(elt.expectedAqls)) 11 | ); 12 | let times = props.mutationData.map((el) => moment(parseInt(el.dateTime))); 13 | console.log(props); 14 | const data = { 15 | labels: times, 16 | datasets: [ 17 | { 18 | label: 'Subscribers', 19 | fill: false, 20 | lineTension: 0.1, 21 | backgroundColor: 'rgba(75,192,192,0.4)', 22 | borderColor: 'dodgerblue', 23 | borderCapStyle: 'butt', 24 | borderDash: [], 25 | borderDashOffset: 0.0, 26 | borderJoinStyle: 'miter', 27 | pointBorderColor: 'dodgerblue', 28 | pointBackgroundColor: '#fff', 29 | pointBorderWidth: 1, 30 | pointHoverRadius: 5, 31 | pointHoverBackgroundColor: 'dodgerblue', 32 | pointHoverBorderColor: 'rgba(220,220,220,1)', 33 | pointHoverBorderWidth: 2, 34 | pointRadius: 1, 35 | pointHitRadius: 10, 36 | data: avgSubscribers, 37 | yAxisID: 'A', 38 | }, 39 | { 40 | label: 'Average Latency', 41 | fill: false, 42 | lineTension: 0.1, 43 | backgroundColor: 'rgba(75,192,192,0.4)', 44 | borderColor: '#40E0D0', 45 | borderCapStyle: 'butt', 46 | borderDash: [], 47 | borderDashOffset: 0.0, 48 | borderJoinStyle: 'miter', 49 | pointBorderColor: 'dodgerblue', 50 | pointBackgroundColor: '#fff', 51 | pointBorderWidth: 1, 52 | pointHoverRadius: 5, 53 | pointHoverBackgroundColor: 'dodgerblue', 54 | pointHoverBorderColor: 'rgba(220,220,220,1)', 55 | pointHoverBorderWidth: 2, 56 | pointRadius: 1, 57 | pointHitRadius: 10, 58 | data: avgLatency, 59 | yAxisID: 'B', 60 | }, 61 | ], 62 | }; 63 | const options = { 64 | legend: { 65 | display: false, 66 | padding: 0, 67 | align: 'center', 68 | position: 'left', 69 | labels: { 70 | boxWidth: 10, 71 | fontColor: 'lightgrey', 72 | }, 73 | }, 74 | scales: { 75 | xAxes: [ 76 | { 77 | type: 'time', 78 | position: 'bottom', 79 | time: { 80 | unit: 'day', 81 | displayFormats: { 82 | day: 'MMM D', 83 | }, 84 | tooltipFormat: 'MMM D, h:mm:ss.SSS a', 85 | }, 86 | distribution: 'series', 87 | ticks: { 88 | maxTicksLimit: 5, 89 | fontColor: 'lightgrey', 90 | fontSize: 9, 91 | }, 92 | }, 93 | ], 94 | yAxes: [ 95 | { 96 | id: 'A', 97 | type: 'linear', 98 | position: 'left', 99 | scaleLabel: { 100 | display: true, 101 | labelString: 'Subscribers', 102 | fontColor: 'dodgerBlue', 103 | }, 104 | ticks: { 105 | fontColor: 'lightgrey', 106 | fontSize: 9, 107 | }, 108 | }, 109 | { 110 | id: 'B', 111 | type: 'linear', 112 | position: 'right', 113 | scaleLabel: { 114 | display: true, 115 | labelString: 'Avg Latency', 116 | fontColor: 'turquoise', 117 | }, 118 | ticks: { 119 | fontColor: 'lightgrey', 120 | fontSize: 9, 121 | }, 122 | }, 123 | ], 124 | }, 125 | }; 126 | 127 | const handleClick = () => { 128 | const newLatency = avgLatency; 129 | newLatency.pop(); 130 | const newSubs = avgSubscribers; 131 | newSubs.pop(); 132 | setAvgLatency(newLatency); 133 | setAvgSubscribers(newSubs); 134 | }; 135 | 136 | return ( 137 |
138 | 139 |
140 | ); 141 | } 142 | 143 | export default LineChart; 144 | -------------------------------------------------------------------------------- /dummydata.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | resolverStats: { newColor: 200, luckyNumber: 300 }, 4 | subscriptionHistory: { 5 | 1601784033001: 1, 6 | 1601785976517: 2, 7 | 1601787564341: 3, 8 | }, 9 | mutations: [ 10 | { 11 | mutationId: '3fcc8d07-c4b5-477a-8dff-487f56dd3d88', 12 | resolver: 'newColor', 13 | avgLatency: 21, 14 | expectedAqls: 1, 15 | dateTime: 1601784033001, 16 | aqls: [ 17 | { 18 | id: 'da7cb4a9-6387-4c0e-9d81-249c35755af1', 19 | mutation_send_time: '1601784033001', 20 | mutation_received_time: '1601784033008', 21 | subscriber_received_time: '1601784033022', 22 | latency: 21, 23 | mutation_id: '3fcc8d07-c4b5-477a-8dff-487f56dd3d88', 24 | resolver: 'newColor', 25 | expected_subscribers: '1', 26 | successful_subscribers: '1', 27 | error: null, 28 | user_token: null, 29 | }, 30 | ], 31 | }, 32 | { 33 | mutationId: '531fab47-e90d-4419-a886-ce311f633773', 34 | resolver: 'newColor', 35 | avgLatency: 89, 36 | expectedAqls: 2, 37 | dateTime: 1601785976517, 38 | aqls: [ 39 | { 40 | id: 'e1ebe5dc-3bae-4d75-95ca-1244db6abe8f', 41 | mutation_send_time: '1601785976517', 42 | mutation_received_time: '1601785976545', 43 | subscriber_received_time: '1601785976553', 44 | latency: 36, 45 | mutation_id: '531fab47-e90d-4419-a886-ce311f633773', 46 | resolver: 'newColor', 47 | expected_subscribers: '2', 48 | successful_subscribers: '2', 49 | error: null, 50 | user_token: null, 51 | }, 52 | { 53 | id: '9bd2720b-9a13-48e8-950e-d101b0758e1d', 54 | mutation_send_time: '1601785976517', 55 | mutation_received_time: '1601785976545', 56 | subscriber_received_time: '1601785976557', 57 | latency: 40, 58 | mutation_id: '531fab47-e90d-4419-a886-ce311f633773', 59 | resolver: 'newColor', 60 | expected_subscribers: '2', 61 | successful_subscribers: '2', 62 | error: null, 63 | user_token: null, 64 | }, 65 | ], 66 | }, 67 | { 68 | mutationId: '966604df-1b4a-48ae-9d54-a99f1c71a404', 69 | resolver: 'newColor', 70 | avgLatency: 72, 71 | expectedAqls: 3, 72 | dateTime: 1601787564341, 73 | aqls: [ 74 | { 75 | id: '61e89e35-5a40-4c8c-9efe-49b91e662dba', 76 | mutation_send_time: '1601787564341', 77 | mutation_received_time: '1601787564346', 78 | subscriber_received_time: '1601787564348', 79 | latency: 7, 80 | mutation_id: '966604df-1b4a-48ae-9d54-a99f1c71a404', 81 | resolver: 'newColor', 82 | expected_subscribers: '3', 83 | successful_subscribers: '3', 84 | error: null, 85 | user_token: null, 86 | }, 87 | { 88 | id: 'b241b477-1ece-43ad-a3a1-17fa40107fb1', 89 | mutation_send_time: '1601787564341', 90 | mutation_received_time: '1601787564346', 91 | subscriber_received_time: '1601787564348', 92 | latency: 7, 93 | mutation_id: '966604df-1b4a-48ae-9d54-a99f1c71a404', 94 | resolver: 'newColor', 95 | expected_subscribers: '3', 96 | successful_subscribers: '3', 97 | error: null, 98 | user_token: null, 99 | }, 100 | { 101 | id: '51926f3c-3c2f-4d8c-bdfd-f9308ba45250', 102 | mutation_send_time: '1601787564341', 103 | mutation_received_time: '1601787564346', 104 | subscriber_received_time: '1601787564348', 105 | latency: 7, 106 | mutation_id: '966604df-1b4a-48ae-9d54-a99f1c71a404', 107 | resolver: 'newColor', 108 | expected_subscribers: '3', 109 | successful_subscribers: '3', 110 | error: null, 111 | user_token: null, 112 | }, 113 | ], 114 | }, 115 | ], 116 | errors: [ 117 | { 118 | mutationId: 'adcebb0e-4e6c-473c-bada-76e8a7962c6a', 119 | resolver: 'newColor', 120 | avgTime: 'N/A', 121 | dateTime: 1601788872468, 122 | expected: 2, 123 | received: 0, 124 | aqls: [], 125 | }, 126 | { 127 | mutationId: '424a35b6-d721-4f35-8acf-cae6e2584bbd', 128 | resolver: 'newColor', 129 | avgTime: '28', 130 | dateTime: 1601790770418, 131 | expected: 2, 132 | received: 1, 133 | aqls: [ 134 | { 135 | id: 'c210ac95-774d-40cd-8aa8-7f93e280e95d', 136 | mutation_send_time: '1601790770418', 137 | mutation_received_time: '1601790770441', 138 | subscriber_received_time: '1601790770446', 139 | latency: 28, 140 | mutation_id: '424a35b6-d721-4f35-8acf-cae6e2584bbd', 141 | resolver: 'newColor', 142 | expected_subscribers: '2', 143 | successful_subscribers: '1', 144 | error: 'true', 145 | user_token: null, 146 | }, 147 | ], 148 | }, 149 | ], 150 | }, 151 | ]; 152 | 153 | export default data; -------------------------------------------------------------------------------- /public/scss/dashboard.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | // DASHBOARD STYLES 4 | 5 | .App { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | font-family: Arial, Helvetica, sans-serif; 11 | } 12 | 13 | ::-webkit-scrollbar { 14 | display: none; 15 | } 16 | 17 | #dashboard-container { 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | padding-top: 100px; 23 | 24 | width: 1024px; 25 | 26 | button { 27 | border-radius: 10%; 28 | background-color: $background-color; 29 | color: $primary-color; 30 | border: 1px solid $secondary-color; 31 | padding: 3, 5; 32 | } 33 | 34 | #top-row { 35 | display: flex; 36 | flex-wrap: wrap; 37 | justify-content: space-between; 38 | align-items: stretch; 39 | height: 35%; 40 | margin-bottom: 1rem; 41 | width: 100%; 42 | //padding: 0.5rem; 43 | 44 | #LineChartContainer { 45 | display: flex; 46 | flex-grow: 1; 47 | flex-direction: column; 48 | justify-content: space-between; 49 | align-items: center; 50 | height: 100%; 51 | border: 1px solid $primary-color; 52 | position: relative; 53 | } 54 | 55 | .LineChart { 56 | //justify-self: flex-end; 57 | // display: flex; 58 | // flex-direction: column; 59 | // justify-content: center; 60 | //width: 70%; 61 | } 62 | } 63 | 64 | #middle-row { 65 | display: flex; 66 | justify-content: space-between; 67 | align-items: stretch; 68 | width: 100%; 69 | height: 260px; 70 | padding-top: 20px; 71 | 72 | #PieChartContainer { 73 | display: flex; 74 | flex-direction: column; 75 | justify-content: center; 76 | align-items: center; 77 | width: 40%; 78 | height: 100%; 79 | border: 1px solid $primary-color; 80 | margin-right: 2rem; 81 | } 82 | 83 | // .PieCanvasDiv { 84 | // // height: 14rem; 85 | // //width: 28rem; 86 | // width: 100%; 87 | // height: auto; 88 | // position: relative; 89 | // } 90 | 91 | // .PieChart { 92 | // position: relative; 93 | // display: flex; 94 | // flex-direction: column; 95 | // align-items: center; 96 | // justify-content: center; 97 | // min-height: 90%; 98 | // min-width: 90%; 99 | // } 100 | 101 | #ErrorLogContainer { 102 | display: flex; 103 | flex-direction: column; 104 | flex-grow: 1; 105 | align-items: center; 106 | color: red; 107 | text-align: center; 108 | overflow: scroll; 109 | position: relative; 110 | border: 1px solid snow; 111 | } 112 | 113 | // #MapContainer { 114 | // display: flex; 115 | // flex-direction: column; 116 | // justify-content: center; 117 | // align-items: center; 118 | // width: 31.5%; 119 | // height: 100%; 120 | // border: 1px solid $primary-color; 121 | // } 122 | 123 | // #map { 124 | // } 125 | 126 | // #NodeContainer { 127 | // display: flex; 128 | // flex-direction: column; 129 | // justify-content: center; 130 | // align-items: center; 131 | // width: 31.5%; 132 | // height: 100%; 133 | // border: 1px solid $primary-color; 134 | // } 135 | 136 | // #node { 137 | // #node-label { 138 | // color: $primary-color; 139 | // } 140 | // } 141 | 142 | // #StatsContainer { 143 | // display: flex; 144 | // flex-direction: column; 145 | // justify-content: center; 146 | // align-items: left; 147 | // width: 31.5%; 148 | // height: 100%; 149 | // border: 1px solid $primary-color; 150 | 151 | // p { 152 | // font-family: Arial, Helvetica, sans-serif; 153 | // color: $primary-color; 154 | // font-size: 0.75rem; 155 | // margin: 10px; 156 | // } 157 | // } 158 | 159 | // #Stats { 160 | // } 161 | } 162 | 163 | #bottom-row { 164 | display: flex; 165 | flex-direction: column; 166 | justify-content: flex-start; 167 | align-items: center; 168 | margin-top: 20px; 169 | width: 100%; 170 | height: auto; 171 | border: 1px solid $primary-color; 172 | } 173 | 174 | #box-titles { 175 | display: flex; 176 | margin-bottom: 0.5rem; 177 | justify-content: center; 178 | align-items: center; 179 | text-align: center; 180 | width: 100%; 181 | color: snow; 182 | background-color: #2a2e42; 183 | height: 2.5rem; 184 | z-index: 3; 185 | } 186 | 187 | #LogContainer { 188 | display: flex; 189 | flex-direction: column; 190 | width: 100%; 191 | align-items: center; 192 | color: snow; 193 | text-align: center; 194 | height: 40vh; 195 | overflow: scroll; 196 | position: relative; 197 | } 198 | 199 | #log-header { 200 | display: flex; 201 | position: -webkit-sticky; 202 | position: sticky; 203 | top: 0; 204 | justify-content: space-between; 205 | align-items: center; 206 | background-color: #2a2e42; 207 | height: 2.5rem; 208 | z-index: 3; 209 | width: 1024px; 210 | 211 | div { 212 | flex: 1 1 0; 213 | } 214 | 215 | .wide-data { 216 | flex-grow: 2; 217 | } 218 | 219 | .spacer { 220 | max-width: 39px; 221 | } 222 | } 223 | 224 | .mutationRows { 225 | margin-top: 3rem; 226 | width: 100%; 227 | } 228 | .mutation-row { 229 | display: flex; 230 | flex-direction: column; 231 | justify-content: space-between; 232 | width: 100%; 233 | align-items: center; 234 | margin-bottom: 0rem; 235 | font-size: 0.8rem; 236 | 237 | div { 238 | flex: 1 1 0; 239 | } 240 | 241 | .wide-data { 242 | flex-grow: 2; 243 | } 244 | 245 | button { 246 | margin-left: 5px; 247 | height: auto; 248 | font-size: 1.5rem; 249 | border: none; 250 | outline: none !important; 251 | } 252 | 253 | .aql-container { 254 | background-color: #2a2e42; 255 | margin: 16px 10rem; 256 | height: inherit; 257 | width: 90%; 258 | div { 259 | flex: 1 1 0; 260 | } 261 | 262 | .wide-data { 263 | flex-grow: 2; 264 | } 265 | 266 | .aql-row-header { 267 | display: flex; 268 | justify-content: space-between; 269 | align-items: center; 270 | width: 100%; 271 | margin: 10px 0; 272 | } 273 | 274 | .aql-row { 275 | display: flex; 276 | justify-content: space-between; 277 | width: 100%; 278 | align-items: center; 279 | margin-bottom: 10px; 280 | background-color: #2a2e42; 281 | } 282 | } 283 | } 284 | 285 | .mutation-data { 286 | display: flex; 287 | justify-content: flex-start; 288 | width: 100%; 289 | align-items: center; 290 | } 291 | } 292 | 293 | .footer { 294 | display: flex; 295 | align-items: center; 296 | height: 3.5rem; 297 | color: snow; 298 | background-color: #2a2e42; 299 | position: fixed; 300 | bottom: 0; 301 | width: 100%; 302 | 303 | ul { 304 | display: flex; 305 | align-items: flex-start; 306 | list-style: none; 307 | 308 | li { 309 | margin-left: 2rem; 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /src/components/TopRow/LineChart-old.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2' 3 | import { 4 | select, 5 | scaleLinear, 6 | scaleTime, 7 | line, 8 | min, 9 | max, 10 | curveCardinal, 11 | axisBottom, 12 | axisLeft, 13 | axisRight, 14 | timeFormat, 15 | zoom, 16 | zoomTransform, 17 | } from 'd3'; 18 | import useResizeObserver from '../useResizeObserver'; 19 | 20 | 21 | 22 | /** 23 | * Component that renders a LineChart 24 | */ 25 | 26 | function LineChart(props) { 27 | 28 | let avgLatency = props.mutationData.map(elt => ({ avgL: elt.avgLatency, mutationDate: parseInt(elt.dateTime), subscribers: parseInt(elt.expectedAqls) })); 29 | let mutationLatencies = props.mutationData.map(elt => elt.avgLatency); 30 | let avgSubscribers = props.mutationData.map(elt => parseInt(elt.expectedAqls)); 31 | let mutationDates = props.mutationData.map(elt => parseInt(elt.dateTime)); 32 | 33 | mutationDates = mutationDates.slice(3); 34 | avgLatency = avgLatency.slice(3); 35 | avgSubscribers = avgSubscribers.slice(3); 36 | mutationLatencies = mutationLatencies.slice(3); 37 | 38 | 39 | // console.log(avgLatency.slice(0, 50)); 40 | 41 | // console.log(mutationDates); 42 | 43 | const chartRef = useRef(); 44 | // const wrapperRef = useRef(); 45 | // const dimensions = useResizeObserver(wrapperRef); 46 | // const [currentZoomState, setCurrentZoomState] = useState(); 47 | 48 | // // will be called initially and on every data change 49 | // useEffect(() => { 50 | // const svg = select(svgRef.current); 51 | // const svgContent = svg.select('.content'); 52 | // console.log(svgContent) 53 | // const { width, height } = wrapperRef.current.getBoundingClientRect(); 54 | // console.log(width, height) 55 | // // scales + line generator 56 | // const xScale = scaleTime() 57 | // .domain([min(mutationDates), max(mutationDates)]) 58 | // .range([0, width]); 59 | 60 | // if (currentZoomState) { 61 | // const newXScale = currentZoomState.rescaleX(xScale); 62 | // xScale.domain(newXScale.domain()); 63 | // } 64 | 65 | // const yScale = scaleLinear() 66 | // .domain([0, max(mutationLatencies) + 10]) 67 | // .range([height - 10, 0]); 68 | 69 | // const ySubScale = scaleLinear() 70 | // .domain([0, max(avgSubscribers)]) 71 | // .range([height - 10, 0]); 72 | 73 | // const latencyLine = line() 74 | // .x(d => xScale(d.mutationDate)) 75 | // .y(d => yScale(d.avgL)); 76 | // //.curve(curveCardinal); 77 | 78 | // const subscriberLine = line() 79 | // .x(d => xScale(d.mutationDate)) 80 | // .y(d => ySubScale(d.subscribers)); 81 | // //.curve(curveCardinal); 82 | 83 | // console.log(svgContent) 84 | // // render the line 85 | // // const children = svgContent.selectChildren() 86 | 87 | // // svgContent.remove(children) 88 | 89 | // svgContent 90 | // .append('path') 91 | // .data([avgLatency]) 92 | // .attr('class', 'myLine') 93 | // .attr('stroke', 'lightblue') 94 | // .attr('fill', 'none') 95 | // .attr('d', latencyLine); 96 | 97 | // svgContent 98 | // .append('path') 99 | // .data([avgLatency]) 100 | // .attr('class', 'myLine') 101 | // .style('stroke', 'dodgerblue') 102 | // .attr('fill', 'none') 103 | // .attr('d', subscriberLine); 104 | 105 | // // svgContent 106 | // // .selectAll('.myDot') 107 | // // .data(avgLatency) 108 | // // .join('circle') 109 | // // .attr('class', 'myDot') 110 | // // .attr('stroke', 'lightblue') 111 | // // .attr('r', 2) 112 | // // .attr('fill', 'lightblue') 113 | // // .attr('cx', (d) => xScale(d.mutationDate)) 114 | // // .attr('cy', (d) => yScale(d.avgL)); 115 | 116 | // // axes 117 | // const xAxis = axisBottom(xScale) 118 | // .tickFormat(timeFormat('%m-%dT%H:%M:%S')); 119 | 120 | // svg 121 | // .select('.x-axis') 122 | // .attr('transform', `translate(0, ${height})`) 123 | // .call(xAxis) 124 | // .style('color', 'white') 125 | // .selectAll('text') 126 | // .attr("transform", "translate(-10,10)rotate(-45)") 127 | // .style("text-anchor", "end") 128 | // // .style("font-size", 20) 129 | 130 | 131 | // const yAxis = axisLeft(yScale); 132 | // svg.select('.y-axis').style('color', 'white').call(yAxis); 133 | 134 | // const ySubAxis = axisRight(ySubScale); 135 | // svg.append('g') 136 | // .style('color', 'white') 137 | // .attr("transform", `translate( ${width},0)`) 138 | // .call(ySubAxis); 139 | 140 | // // zoom 141 | // const zoomBehavior = zoom() 142 | // .scaleExtent([0.5, 5]) 143 | // .translateExtent([ 144 | // [0, 0], 145 | // [width, height], 146 | // ]) 147 | // .on('zoom', () => { 148 | // const zoomState = zoomTransform(svg.node()); 149 | // setCurrentZoomState(zoomState); 150 | // }); 151 | 152 | // // Add the labels to x and y-axes 153 | // svg.append("text") 154 | // .attr("text-anchor", "end") 155 | // .attr("transform", "rotate(-90)") 156 | // .attr("y", -50) 157 | // .attr("x", -30) 158 | // .text("Average Latency") 159 | // .style("font-family", "Arial") 160 | // .style("font-size", "12px") 161 | // .style("fill", "lightblue"); 162 | 163 | // svg.append("text") 164 | // .attr("text-anchor", "end") 165 | // .attr("transform", "rotate(-90)") 166 | // .attr("y", `${width + 30}`) 167 | // .attr("x", -12) 168 | // .text("Number of Subscribers") 169 | // .style("font-family", "Arial") 170 | // .style("font-size", "12px") 171 | // .style("fill", "dodgerblue"); 172 | 173 | // // svg.append("text") 174 | // // .attr("text-anchor", "end") 175 | // // .attr("y", 260) 176 | // // .attr("x", 210) 177 | // // .text("Time") 178 | // // .style("font-family", "Arial") 179 | // // .style("font-size", "12px") 180 | // // .style("fill", "white"); 181 | 182 | // svg.call(zoomBehavior); 183 | // }, [currentZoomState, avgLatency, dimensions]); 184 | const data = { 185 | labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], 186 | datasets: [ 187 | { 188 | label: 'My First dataset', 189 | fill: false, 190 | lineTension: 0.1, 191 | backgroundColor: 'rgba(75,192,192,0.4)', 192 | borderColor: 'rgba(75,192,192,1)', 193 | borderCapStyle: 'butt', 194 | borderDash: [], 195 | borderDashOffset: 0.0, 196 | borderJoinStyle: 'miter', 197 | pointBorderColor: 'rgba(75,192,192,1)', 198 | pointBackgroundColor: '#fff', 199 | pointBorderWidth: 1, 200 | pointHoverRadius: 5, 201 | pointHoverBackgroundColor: 'rgba(75,192,192,1)', 202 | pointHoverBorderColor: 'rgba(220,220,220,1)', 203 | pointHoverBorderWidth: 2, 204 | pointRadius: 1, 205 | pointHitRadius: 10, 206 | data: [65, 59, 80, 81, 56, 55, 40] 207 | } 208 | ] 209 | }; 210 | 211 | return ( 212 | <> 213 | 214 | 215 | {/*
216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 |
*/} 227 | 228 | ); 229 | } 230 | 231 | export default LineChart; 232 | --------------------------------------------------------------------------------