├── OSSMETADATA ├── .babelrc ├── src ├── favicon.ico ├── components │ ├── vizceral-mark.png │ ├── locator.css │ ├── physicsOptions.css │ ├── trafficFlow.css │ ├── updateStatus.css │ ├── breadcrumbs.css │ ├── optionsPanel.css │ ├── controls.css │ ├── filterActions.js │ ├── notices.jsx │ ├── detailsSubpanelSubNodes.jsx │ ├── stepper.jsx │ ├── displayOptions.jsx │ ├── detailsPanelConnection.jsx │ ├── breadcrumbs.jsx │ ├── filterControls.jsx │ ├── locator.jsx │ ├── detailsSubpanel.jsx │ ├── loadingCover.css │ ├── loadingCover.jsx │ ├── detailsPanel.css │ ├── optionsPanel.jsx │ ├── updateStatus.jsx │ ├── detailsPanelNode.jsx │ ├── filterStore.js │ ├── physicsOptions.jsx │ ├── connectionList.jsx │ ├── subNodeList.jsx │ └── trafficFlow.jsx ├── appDispatcher.js ├── appConstants.js ├── app.jsx ├── sample_data_simple.json ├── index.html ├── app.css └── sample_data_region_only.json ├── .gitignore ├── Dockerfile ├── .eslintrc ├── README.md ├── webpack.config.js ├── package.json └── LICENSE /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=archived 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "react", "env", "stage-0" ] 3 | } 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix/vizceral-example/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/components/vizceral-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix/vizceral-example/HEAD/src/components/vizceral-mark.png -------------------------------------------------------------------------------- /src/components/locator.css: -------------------------------------------------------------------------------- 1 | .locator-input { 2 | height: 24px; 3 | width: 200px; 4 | background-color: var(--colorPageBackground); 5 | color: var(--colorText); 6 | border-color: var(--colorConnectionLine); 7 | border-radius: 4px; 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | .DS_Store 3 | 4 | # IDE config 5 | /.idea 6 | 7 | # Generated 8 | /.jshintignore 9 | /.jshintrc 10 | /dist 11 | npm-debug.log 12 | node_modules 13 | 14 | # Typings 15 | *.ts 16 | 17 | # Temporary Files and Backups 18 | *.swp 19 | *.*~ 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | # Install app dependencies 7 | COPY package.json . 8 | RUN npm install 9 | 10 | # Bundle app source 11 | COPY . . 12 | 13 | EXPOSE 8080 14 | CMD [ "npm", "run", "dev" ] 15 | -------------------------------------------------------------------------------- /src/components/physicsOptions.css: -------------------------------------------------------------------------------- 1 | .physics-options > div { 2 | white-space: nowrap; 3 | } 4 | .physics-options > div > label { 5 | display: inline-block; 6 | width: 160px; 7 | } 8 | .physics-options > div > input[type=checkbox] { 9 | margin-right: 5px; 10 | } 11 | .physics-options > div > input[type=number] { 12 | width: 60px; 13 | } 14 | -------------------------------------------------------------------------------- /src/appDispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Dispatcher } from 'flux'; 4 | 5 | class AppDispatcher extends Dispatcher { 6 | handleAction (action) { 7 | this.dispatch({ 8 | source: 'VIEW_ACTION', 9 | action: action 10 | }); 11 | } 12 | } 13 | 14 | const appDispatcher = new AppDispatcher(); 15 | 16 | export default appDispatcher; 17 | -------------------------------------------------------------------------------- /src/components/trafficFlow.css: -------------------------------------------------------------------------------- 1 | .selected-formatted-date-time, 2 | .reset-layout-link { 3 | display: inline-block; 4 | line-height: 24px; 5 | color: var(--colorNormalDimmed); 6 | } 7 | .reset-layout-link { 8 | padding-left: 15px; 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/appConstants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import keymirror from 'keymirror'; 4 | 5 | const AppConstants = { 6 | ActionTypes: keymirror({ 7 | TRAFFIC_DATA_RECEIVED: null, 8 | SERVER_ACTION: null, 9 | VIEW_ACTION: null, 10 | 11 | UPDATE_FILTER: null, 12 | RESET_FILTERS: null, 13 | CLEAR_FILTERS: null 14 | }) 15 | }; 16 | 17 | export default AppConstants; 18 | -------------------------------------------------------------------------------- /src/components/updateStatus.css: -------------------------------------------------------------------------------- 1 | .update-status { 2 | display: inline-block; 3 | padding: 0 7px; 4 | } 5 | 6 | #update-status .header { 7 | margin: 0; 8 | font-weight: 700; 9 | } 10 | 11 | .table-borderless tbody tr td, .table-borderless tbody tr th, .table-borderless thead tr th { 12 | border: none; 13 | padding: 0; 14 | } 15 | 16 | .table-borderless tbody tr td:first-child { 17 | text-align: right; 18 | } 19 | 20 | .table-borderless tbody tr td:last-of-type { 21 | text-align: left; 22 | padding-left: 3px; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/breadcrumbs.css: -------------------------------------------------------------------------------- 1 | .breadcrumbs ol { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .breadcrumbs li { 8 | color: var(--colorNormal); 9 | font-weight: 200; 10 | font-size: 20px; 11 | display: inline; 12 | } 13 | 14 | .breadcrumbs li:last-of-type { 15 | font-weight: 600; 16 | } 17 | 18 | .breadcrumbs li a { 19 | color: var(--colorNormal); 20 | text-decoration: none; 21 | } 22 | 23 | .breadcrumbs li:after { 24 | content: ' / '; 25 | } 26 | 27 | .breadcrumbs li:last-child:after { 28 | content: none; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/optionsPanel.css: -------------------------------------------------------------------------------- 1 | .options-panel { 2 | display: inline-block; 3 | padding-left: 15px; 4 | position: relative; 5 | -webkit-user-select: none; 6 | -moz-user-select: none; 7 | -ms-user-select: none; 8 | user-select: none; 9 | } 10 | 11 | .options-panel-title { 12 | line-height: 24px; 13 | } 14 | 15 | .options-panel-content { 16 | padding: 10px; 17 | display: block; 18 | position: absolute; 19 | background-color: var(--colorPageBackground); 20 | z-index: 9999; 21 | width: 250px; 22 | } 23 | 24 | .options-link { 25 | color: var(--colorNormalDimmed); 26 | text-decoration: none; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/controls.css: -------------------------------------------------------------------------------- 1 | .vizceral-control { 2 | padding: 5px 10px 5px 10px; 3 | border-bottom: 1px solid var(--colorBorderLines); 4 | width: 100%; 5 | } 6 | 7 | .vizceral-control span { 8 | padding-right: 5px; 9 | } 10 | 11 | .vizceral-controls-panel div, .vizceral-controls-panel input[type=range] { 12 | display: inline-block; 13 | } 14 | 15 | .vizceral-controls-panel input[type=checkbox] { 16 | padding: 0 3px 0 3px; 17 | } 18 | 19 | .vizceral-controls-panel input[type=radio] { 20 | margin-right: 5px; 21 | } 22 | 23 | .vizceral-controls-panel .radio-control:not(:first-child) { 24 | margin-left: 5px; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/components/filterActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import AppDispatcher from '../appDispatcher'; 4 | import AppConstants from '../appConstants'; 5 | 6 | export default { 7 | updateFilter: (filters) => { 8 | AppDispatcher.handleAction({ 9 | actionType: AppConstants.ActionTypes.UPDATE_FILTER, 10 | data: filters 11 | }); 12 | }, 13 | resetFilters: () => { 14 | AppDispatcher.handleAction({ 15 | actionType: AppConstants.ActionTypes.RESET_FILTERS 16 | }); 17 | }, 18 | clearFilters: () => { 19 | AppDispatcher.handleAction({ 20 | actionType: AppConstants.ActionTypes.CLEAR_FILTERS 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import 'bootstrap'; 4 | import 'bootstrap/dist/css/bootstrap.css'; 5 | import React from 'react'; // eslint-disable-line no-unused-vars 6 | import ReactDOM from 'react-dom'; 7 | import WebFont from 'webfontloader'; 8 | 9 | import './app.css'; 10 | import TrafficFlow from './components/trafficFlow'; 11 | 12 | function fontsActive () { 13 | ReactDOM.render( 14 | , 15 | document.getElementById('traffic') 16 | ); 17 | } 18 | 19 | // Only load the app once we have the webfonts. 20 | // This is necessary since we use the fonts for drawing on Canvas'... 21 | 22 | // imports are loaded and elements have been registered 23 | 24 | WebFont.load({ 25 | custom: { 26 | families: ['Source Sans Pro:n3,n4,n6,n7'], 27 | urls: ['/fonts/source-sans-pro.css'] 28 | }, 29 | active: fontsActive 30 | }); 31 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "mocha": true, 5 | "node": false, 6 | "amd": true 7 | }, 8 | "parser": "babel-eslint", 9 | "extends": [ 10 | "airbnb-base" 11 | ], 12 | "rules": { 13 | "indent": [ 14 | 2, 15 | 2 16 | ], 17 | "new-cap": 0, 18 | "class-methods-use-this": 0, 19 | "comma-dangle": 0, 20 | "func-names": 0, 21 | "import/extensions": [1, "never"], 22 | "import/no-unresolved": 0, 23 | "import/no-webpack-loader-syntax": 0, 24 | "no-plusplus": 0, 25 | "no-var": 1, 26 | "space-before-function-paren": [1, "always"], 27 | "vars-on-top": 0, 28 | "no-param-reassign": 0, 29 | "no-underscore-dangle": 0, 30 | "max-len": 0, 31 | "react/jsx-uses-vars": 1, 32 | "object-shorthand": [0, "always"], 33 | "strict": 0 34 | }, 35 | "plugins": [ 36 | "react" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://raw.githubusercontent.com/Netflix/vizceral/master/logo.png) 2 | 3 | # Vizceral Example 4 | This is a sample application using the [React wrapper](https://github.com/Netflix/vizceral-react) around the [vizceral](https://github.com/Netflix/vizceral) graph. 5 | For more details about using vizceral in your own projects with your own data, refer to the above repositories. 6 | 7 | # Setup 8 | 1. Get source, install deps, and run demo server. 9 | 10 | ```sh 11 | git clone git@github.com:Netflix/vizceral-example.git 12 | cd vizceral-example 13 | npm install 14 | npm run dev 15 | ``` 16 | 17 | 2. Open `localhost:8080` in your browser. 18 | 19 | ##### Using Docker 20 | If you don't have a node environment setup or would like to run this example on a platform, there is a Dockerfile for experimental usage. 21 | 22 | ``` 23 | $ docker build -t /vizceral-example . 24 | ``` 25 | ``` 26 | $ docker run -p 41911:8080 -d /vizceral-example 27 | ``` 28 | 29 | Then you should be able to navigate to http://localhost:41911 30 | -------------------------------------------------------------------------------- /src/components/notices.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | class Notices extends React.Component { 7 | constructor (props) { 8 | super(props); 9 | this.state = { 10 | notices: props.notices 11 | }; 12 | } 13 | 14 | componentWillReceiveProps (nextProps) { 15 | this.setState({ 16 | notices: nextProps.notices 17 | }); 18 | } 19 | 20 | render () { 21 | return ( 22 |
Notices  23 | { 24 | this.state.notices.length > 0 ? this.state.notices.map((notice) => { 25 | let noticeTitle = notice.title; 26 | if (notice.link) { 27 | noticeTitle = {notice.title} ; 28 | } 29 | return
{noticeTitle}
; 30 | }) : (
None
) 31 | } 32 |
33 | ); 34 | } 35 | } 36 | 37 | Notices.propTypes = { 38 | notices: PropTypes.array.isRequired 39 | }; 40 | 41 | export default Notices; 42 | -------------------------------------------------------------------------------- /src/sample_data_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "renderer": "global", 3 | "name": "edge", 4 | "nodes": [ 5 | { 6 | "renderer": "region", 7 | "name": "INTERNET", 8 | "class": "normal" 9 | }, 10 | { 11 | "renderer": "region", 12 | "name": "us-east-1", 13 | "maxVolume": 50000, 14 | "class": "normal", 15 | "updated": 1466838546805, 16 | "nodes": [ 17 | { 18 | "name": "INTERNET", 19 | "renderer": "focusedChild", 20 | "class": "normal" 21 | }, 22 | { 23 | "name": "proxy-prod", 24 | "renderer": "focusedChild", 25 | "class": "normal" 26 | } 27 | ], 28 | "connections": [ 29 | { 30 | "source": "INTERNET", 31 | "target": "proxy-prod", 32 | "metrics": { 33 | "danger": 116.524, 34 | "normal": 15598.906 35 | }, 36 | "class": "normal" 37 | } 38 | ] 39 | } 40 | ], 41 | "connections": [ 42 | { 43 | "source": "INTERNET", 44 | "target": "us-east-1", 45 | "metrics": { 46 | "normal": 26037.626, 47 | "danger": 92.37 48 | }, 49 | "notices": [ 50 | ], 51 | "class": "normal" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /src/components/detailsSubpanelSubNodes.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import SubNodeList from './subNodeList'; 7 | import DetailsSubpanel from './detailsSubpanel'; 8 | 9 | class DetailsSubpanelSubNodes extends React.Component { 10 | constructor (props) { 11 | super(props); 12 | this.state = { 13 | nodes: props.nodes, 14 | region: props.region 15 | }; 16 | } 17 | 18 | componentWillReceiveProps (nextProps) { 19 | this.setState({ 20 | nodes: nextProps.nodes, 21 | region: nextProps.region 22 | }); 23 | } 24 | 25 | render () { 26 | return ( 27 | 28 | { this.state.nodes 29 | ?
30 |
Traffic by Sub Node
31 | 32 |
: undefined } 33 |
34 | ); 35 | } 36 | } 37 | 38 | DetailsSubpanelSubNodes.propTypes = { 39 | nodes: PropTypes.array, 40 | region: PropTypes.string.isRequired 41 | 42 | }; 43 | 44 | export default DetailsSubpanelSubNodes; 45 | -------------------------------------------------------------------------------- /src/components/stepper.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | class Stepper extends React.Component { 7 | constructor (props) { 8 | super(props); 9 | this.state = { 10 | }; 11 | } 12 | 13 | stepChanged (index) { 14 | this.props.changeCallback(index); 15 | } 16 | 17 | render () { 18 | return ( 19 |
    20 | { 21 | this.props.steps.map((step, index) => { 22 | let className = this.props.selectedStep === index ? 'is-current' : undefined; 23 | className = className || this.props.selectedStep > index ? 'is-lower' : undefined; 24 | if (className === 'is-lower' && this.props.selectedStep > index) { className += ' show-bar'; } 25 | let stepName = step.name ? step.name.trim() : undefined; 26 | stepName = stepName || ' '; 27 | return ( 28 |
  1. this.stepChanged(index)} dangerouslySetInnerHTML={{ __html: stepName }}> 29 |
  2. 30 | ); 31 | }) 32 | } 33 |
34 | ); 35 | } 36 | } 37 | 38 | Stepper.propTypes = { 39 | steps: PropTypes.array.isRequired, 40 | selectedStep: PropTypes.number.isRequired, 41 | changeCallback: PropTypes.func.isRequired 42 | }; 43 | 44 | export default Stepper; 45 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vizceral 9 | 10 | 11 | 36 | 37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/displayOptions.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | class DisplayOptions extends React.Component { 7 | constructor (props) { 8 | super(props); 9 | this.state = { 10 | allowDraggingOfNodes: false, 11 | showLabels: true 12 | }; 13 | } 14 | 15 | componentWillReceiveProps (nextProps) { 16 | this.setState(nextProps.options); 17 | } 18 | 19 | _onCheckBoxChanged (event) { 20 | const checkBox = event.target; 21 | const statePropName = checkBox.id; 22 | const newState = {}; 23 | newState[statePropName] = checkBox.checked; 24 | this.setState(newState); 25 | this.props.changedCallback(newState); 26 | } 27 | 28 | render () { 29 | const { allowDraggingOfNodes, showLabels } = this.state; 30 | return ( 31 |
32 |
33 | this._onCheckBoxChanged(event)}/> 34 | 35 |
36 |
37 | this._onCheckBoxChanged(event)}/> 38 | 39 |
40 |
41 | ); 42 | } 43 | } 44 | 45 | DisplayOptions.propTypes = { 46 | options: PropTypes.object.isRequired, 47 | changedCallback: PropTypes.func.isRequired 48 | }; 49 | 50 | export default DisplayOptions; 51 | -------------------------------------------------------------------------------- /src/components/detailsPanelConnection.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-restricted-syntax: 0 */ 2 | 3 | 'use strict'; 4 | 5 | import React from 'react'; 6 | import PropTypes from 'prop-types'; 7 | 8 | import Notices from './notices'; 9 | 10 | import './detailsPanel.css'; 11 | 12 | class DetailsPanelConnection extends React.Component { 13 | constructor (props) { 14 | super(props); 15 | this.state = { 16 | connection: props.connection, 17 | region: props.region 18 | }; 19 | } 20 | 21 | componentWillReceiveProps (nextProps) { 22 | const newState = { 23 | region: nextProps.region, 24 | connection: nextProps.connection 25 | }; 26 | 27 | this.setState(newState); 28 | } 29 | 30 | render () { 31 | const { connection } = this.state; 32 | const notices = (connection && connection.notices) || []; 33 | 34 | return ( 35 |
36 |
37 |
{connection.getName()} 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 | ); 46 | } 47 | } 48 | 49 | DetailsPanelConnection.propTypes = { 50 | closeCallback: PropTypes.func.isRequired, 51 | connection: PropTypes.object.isRequired, 52 | nodeClicked: PropTypes.func, 53 | region: PropTypes.string 54 | }; 55 | 56 | DetailsPanelConnection.defaultProps = { 57 | nodeClicked: () => {}, 58 | region: '' 59 | }; 60 | 61 | export default DetailsPanelConnection; 62 | -------------------------------------------------------------------------------- /src/components/breadcrumbs.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import './breadcrumbs.css'; 7 | 8 | class Breadcrumbs extends React.Component { 9 | constructor (props) { 10 | super(props); 11 | this.state = { 12 | }; 13 | } 14 | 15 | handleClick (index) { 16 | const newState = this.props.navigationStack.slice(0, index + 1); 17 | this.props.navigationCallback(newState); 18 | } 19 | 20 | shouldComponentUpdate (nextProps) { 21 | if (nextProps.navigationStack) { 22 | if (nextProps.navigationStack.length !== this.props.navigationStack) { 23 | return true; 24 | } 25 | 26 | for (let i = 0; i < this.props.navigationStack.length; i++) { 27 | if (nextProps.navigationStack[i] !== this.props.navigationStack[i]) { 28 | return true; 29 | } 30 | } 31 | } 32 | return false; 33 | } 34 | 35 | render () { 36 | const navStack = this.props.navigationStack.slice() || []; 37 | navStack.unshift(this.props.rootTitle); 38 | 39 | return ( 40 |
41 |
    42 | { 43 | navStack.map((state, index) => ((index !== navStack.length - 1) 44 | ?
  1. { this.handleClick(index - 1); }}>{ state }
  2. 45 | :
  3. { state }
  4. )) 46 | } 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | Breadcrumbs.propTypes = { 54 | rootTitle: PropTypes.string.isRequired, 55 | navigationStack: PropTypes.array.isRequired, 56 | navigationCallback: PropTypes.func.isRequired 57 | }; 58 | 59 | export default Breadcrumbs; 60 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* globals __dirname process module */ 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | 9 | module.exports = { 10 | devtool: 'source-map', 11 | entry: './src/app.jsx', 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: 'vizceral.[hash].bundle.js' 15 | }, 16 | resolve: { 17 | alias: { 18 | react: path.resolve('node_modules/react'), 19 | }, 20 | extensions: ['.jsx', '.js'], 21 | modules: ['node_modules'], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.jsx?$/, 27 | loader: 'babel-loader', 28 | exclude: /node_modules/, 29 | }, 30 | { test: /\.woff2?$/, use: 'url-loader?limit=10000&mimetype=application/font-woff' }, 31 | { test: /\.otf$/, use: 'file-loader' }, 32 | { test: /\.ttf$/, use: 'file-loader' }, 33 | { test: /\.eot$/, use: 'file-loader' }, 34 | { test: /\.svg$/, use: 'file-loader' }, 35 | { test: /\.html$/, use: 'html-loader' }, 36 | { test: /\.css$/, use: [{ loader: 'style-loader' }, { loader: 'css-loader' }] } 37 | ] 38 | }, 39 | plugins: [ 40 | new webpack.ProvidePlugin({ 41 | // Automtically detect jQuery and $ as free var in modules 42 | // and inject the jquery library 43 | // This is required by many jquery plugins 44 | jQuery: 'jquery', 45 | $: 'jquery' 46 | }), 47 | new webpack.DefinePlugin({ 48 | __HIDE_DATA__: !!process.env.HIDE_DATA 49 | }), 50 | new HtmlWebpackPlugin({ 51 | title: 'Vizceral', 52 | template: './src/index.html', 53 | favicon: './src/favicon.ico', 54 | inject: true 55 | }) 56 | ] 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/filterControls.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import filterStore from './filterStore'; 5 | import filterActions from './filterActions'; 6 | import Stepper from './stepper'; 7 | 8 | import './controls.css'; 9 | 10 | class FilterControls extends React.Component { 11 | constructor (props) { 12 | super(props); 13 | this.state = { 14 | filters: filterStore.getFilters(), 15 | states: filterStore.getStates() 16 | }; 17 | } 18 | 19 | componentDidMount () { 20 | filterStore.addChangeListener(this.onChange.bind(this)); 21 | } 22 | 23 | componentWillUnmount () { 24 | filterStore.removeChangeListener(this.onChange.bind(this)); 25 | } 26 | 27 | onChange () { 28 | this.setState({ 29 | filters: filterStore.getFilters() 30 | }); 31 | } 32 | 33 | rpsChanged (step) { 34 | filterActions.updateFilter({ rps: this.state.states.rps[step].value }); 35 | } 36 | 37 | resetFilters () { 38 | filterActions.resetFilters(); 39 | } 40 | 41 | render () { 42 | const defaultFilters = filterStore.isDefault(); 43 | 44 | return ( 45 |
46 |
47 | RPS 48 | { this.rpsChanged(step); }} /> 49 |
50 |
51 | 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | FilterControls.propTypes = { 59 | }; 60 | 61 | export default FilterControls; 62 | -------------------------------------------------------------------------------- /src/components/locator.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import './locator.css'; 7 | 8 | const style = { 9 | display: 'inline-block', 10 | position: 'relative' 11 | }; 12 | 13 | const listStyle = { 14 | display: 'inline-block', 15 | position: 'relative', 16 | paddingRight: '5px' 17 | }; 18 | 19 | class Locator extends React.Component { 20 | constructor (props) { 21 | super(props); 22 | this.state = { 23 | }; 24 | } 25 | 26 | locatorChanged (value) { 27 | this.props.changeCallback(value); 28 | } 29 | 30 | clearFilterClicked () { 31 | if (this.props.clearFilterCallback) { 32 | this.props.clearFilterCallback(); 33 | } 34 | } 35 | 36 | render () { 37 | const totalServices = this.props.matches.totalMatches > -1 ? this.props.matches.totalMatches : this.props.matches.total; 38 | const filteredServices = totalServices - (this.props.matches.visibleMatches > -1 ? this.props.matches.visibleMatches : this.props.matches.visible); 39 | 40 | return ( 41 |
42 |
{totalServices} services / {filteredServices} filtered   43 | { filteredServices > 0 44 | ? (show) 45 | : undefined 46 | } 47 |
48 |
49 | { this.locatorChanged(event.currentTarget.value); }} value={this.props.searchTerm} /> 50 | 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | Locator.propTypes = { 58 | searchTerm: PropTypes.string.isRequired, 59 | changeCallback: PropTypes.func.isRequired, 60 | clearFilterCallback: PropTypes.func, 61 | matches: PropTypes.shape({ 62 | total: PropTypes.number, 63 | totalMatches: PropTypes.number, 64 | visible: PropTypes.number, 65 | visibleMatches: PropTypes.number 66 | }) 67 | }; 68 | 69 | export default Locator; 70 | -------------------------------------------------------------------------------- /src/components/detailsSubpanel.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | class DetailsSubpanel extends React.Component { 7 | constructor (props) { 8 | super(props); 9 | this.state = { 10 | expanded: props.expanded ? props.expanded : false 11 | }; 12 | } 13 | 14 | componentWillReceiveProps (nextProps) { 15 | this.setState({ expanded: nextProps.expanded }); 16 | } 17 | 18 | render () { 19 | const { badge } = this.props; 20 | const title = this.props.title.replace(/\s/g, '_'); 21 | const headingId = `${title}Heading`; 22 | const collapseId = `collapse${title}`; 23 | 24 | const { expanded } = this.state; 25 | const iconClass = `glyphicon ${expanded ? 'glyphicon-chevron-down' : 'glyphicon-chevron-right'}`; 26 | const iconStyle = { 27 | fontSize: '12px', 28 | paddingRight: expanded ? '5px' : undefined 29 | }; 30 | return ( 31 |
32 |
33 | 40 |
41 |
42 |
43 | {this.props.children} 44 |
45 |
46 |
47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | DetailsSubpanel.propTypes = { 54 | title: PropTypes.string.isRequired, 55 | expanded: PropTypes.bool, 56 | badge: PropTypes.number 57 | }; 58 | 59 | export default DetailsSubpanel; 60 | -------------------------------------------------------------------------------- /src/components/loadingCover.css: -------------------------------------------------------------------------------- 1 | .loading-cover-wrapper { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .loading-cover { 8 | text-align: center; 9 | position: absolute; 10 | background-color: var(--colorPageBackground); 11 | width: 100%; 12 | height: 100%; 13 | white-space: nowrap; 14 | 15 | -webkit-animation: fadein 2s; /* Safari, Chrome and Opera > 12.1 */ 16 | -moz-animation: fadein 2s; /* Firefox < 16 */ 17 | -ms-animation: fadein 2s; /* Internet Explorer */ 18 | -o-animation: fadein 2s; /* Opera < 12.1 */ 19 | animation: fadein 2s; 20 | } 21 | 22 | .loading-image { 23 | display: block; 24 | 25 | -webkit-animation: saturate 4s; /* Safari, Chrome and Opera > 12.1 */ 26 | -moz-animation: saturate 4s; /* Firefox < 16 */ 27 | -ms-animation: saturate 4s; /* Internet Explorer */ 28 | -o-animation: saturate 4s; /* Opera < 12.1 */ 29 | animation: saturate 4s; 30 | } 31 | 32 | @keyframes saturate { 33 | 0% { 34 | filter: saturate(0); 35 | -webkit-filter: saturate(0); 36 | } 37 | 50% { 38 | filter: saturate(0); 39 | -webkit-filter: saturate(0); 40 | } 41 | 100% { 42 | filter: saturate(1); 43 | -webkit-filter: saturate(1); 44 | } 45 | } 46 | 47 | @-moz-keyframes saturate { 48 | 0% { filter: saturate(0); } 49 | 50% { filter: saturate(0); } 50 | 100% { filter: saturate(1); } 51 | } 52 | 53 | @-webkit-keyframes saturate { 54 | 0% { -webkit-filter: saturate(0); } 55 | 50% { -webkit-filter: saturate(0); } 56 | 100% { -webkit-filter: saturate(1); } 57 | } 58 | 59 | @-o-keyframes saturate { 60 | 0% { -webkit-filter: saturate(0); } 61 | 50% { -webkit-filter: saturate(0); } 62 | 100% { -webkit-filter: saturate(1); } 63 | } 64 | 65 | /* Firefox < 16 */ 66 | @-moz-keyframes fadein { 67 | from { filter: saturate(0); } 68 | to { filter: saturate(1); } 69 | } 70 | 71 | 72 | /* Safari, Chrome and Opera > 12.1 */ 73 | @-webkit-keyframes fadein { 74 | from { opacity: 0; } 75 | to { opacity: 1; } 76 | } 77 | 78 | /* Opera < 12.1 */ 79 | @-o-keyframes fadein { 80 | from { opacity: 0; } 81 | to { opacity: 1; } 82 | } 83 | 84 | @keyframes fadein { 85 | from { opacity: 0; } 86 | to { opacity: 1; } 87 | } 88 | -------------------------------------------------------------------------------- /src/components/loadingCover.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import TWEEN from '@tweenjs/tween.js'; 6 | 7 | import './loadingCover.css'; 8 | 9 | const logo = require('url-loader!./vizceral-mark.png'); // eslint-disable-line import/no-extraneous-dependencies 10 | 11 | const helperStyles = { 12 | display: 'inline-block', 13 | height: '100%', 14 | verticalAlign: 'middle' 15 | }; 16 | 17 | const loaderStyles = { 18 | display: 'inline-block', 19 | verticalAlign: 'middle', 20 | fontSize: '1.5em', 21 | color: '#555' 22 | }; 23 | 24 | 25 | class LoadingCover extends React.Component { 26 | constructor (props) { 27 | super(props); 28 | this.state = { 29 | show: props.show, 30 | showing: props.show 31 | }; 32 | } 33 | 34 | componentWillReceiveProps (nextProps) { 35 | if (nextProps.show !== this.props.show) { 36 | if (!nextProps.show) { 37 | // If transitioning to not show... 38 | // TODO: Figure out how to do this with pure CSS instead of javascript 39 | const data = { opacity: 1 }; 40 | this.tween = new TWEEN.Tween(data) 41 | .to({ opacity: 0 }, 1000) 42 | .easing(TWEEN.Easing.Linear.None) 43 | .onUpdate(() => { 44 | this.setState({ opacity: data.opacity }); 45 | }) 46 | .onComplete(() => { 47 | this.setState({ showing: false }); 48 | }) 49 | .start(); 50 | } else { 51 | // If transitioning to show... 52 | this.setState({ showing: true }); 53 | } 54 | } 55 | } 56 | 57 | render () { 58 | const wrapperStyles = { 59 | display: this.state.showing ? 'initial' : 'none' 60 | }; 61 | 62 | const coverStyles = { 63 | opacity: this.state.showing ? this.state.opacity : undefined 64 | }; 65 | 66 | return ( 67 |
68 | { this.state.showing 69 | ?
70 | 71 |
72 | 73 | Loading... 74 |
75 |
76 | : undefined 77 | } 78 |
79 | ); 80 | } 81 | } 82 | 83 | LoadingCover.propTypes = { 84 | show: PropTypes.bool.isRequired 85 | }; 86 | 87 | export default LoadingCover; 88 | -------------------------------------------------------------------------------- /src/components/detailsPanel.css: -------------------------------------------------------------------------------- 1 | /* Details Panel 2 | -------------------------------------------------- */ 3 | .details-panel { 4 | background-color: rgb(57, 57, 57); 5 | border: 1px solid var(--colorBorderLines); 6 | border-radius: 3px; 7 | width: 350px; 8 | /*height: 100%;*/ 9 | position: absolute; 10 | right: 25px; 11 | top: 25px; 12 | bottom: 25px; 13 | } 14 | 15 | .details-panel-collapsed { 16 | width: 0; 17 | } 18 | 19 | .details-panel-title { 20 | padding-top: 10px; 21 | padding-bottom: 10px; 22 | font-size: 21px; 23 | font-weight: 600; 24 | color: var(--colorNormal); 25 | border-bottom: 1px solid var(--colorBorderLines); 26 | } 27 | 28 | .details-panel-subtitle { 29 | margin-left: -10px; 30 | } 31 | 32 | .details-panel-description { 33 | padding-top: 5px; 34 | padding-bottom: 10px; 35 | } 36 | 37 | .details-panel-close { 38 | font-family: 'Glyphicons Halflings'; /* essential for enabling glyphicon */ 39 | content: "\e114"; 40 | font-size: 12px; 41 | position:absolute; 42 | top: 6px; 43 | right: 10px; 44 | padding: 10px 5px 0 10px; 45 | cursor: pointer; 46 | } 47 | 48 | .details-panel-subpanel .panel-group .panel { 49 | border-left: 0; 50 | border-right: 0; 51 | } 52 | 53 | .details-panel-subpanel .panel-title { 54 | height: 18px; 55 | font-size: 16px; 56 | font-weight: 600; 57 | } 58 | 59 | .details-panel-subpanel .panel-title .badge { 60 | margin-top: -3px; 61 | } 62 | 63 | .metric { 64 | color: var(--colorText); 65 | } 66 | 67 | .metric-title { 68 | color: var(--colorText); 69 | } 70 | 71 | .metric-query { 72 | width: 100%; 73 | word-wrap: break-word; 74 | } 75 | 76 | .table-container > .table > thead > tr > th { 77 | padding: 0; 78 | border-bottom: none; 79 | } 80 | 81 | .connection-list .table-container, .subnode-list .table-container { 82 | max-height: 200px; 83 | overflow: auto; 84 | } 85 | 86 | .connection-list .ReactVirtualized__Table__row:focus { 87 | outline:0; 88 | } 89 | 90 | .table-header > .table > thead > tr > th { 91 | padding: 5px; 92 | } 93 | 94 | .react-bs-container .table-header { 95 | height: 30px; 96 | } 97 | 98 | .react-bs-container .table-container > table { 99 | margin-top: -25px !important; 100 | } 101 | 102 | .table-container > table > tbody > tr > td { 103 | line-height: 20px !important; 104 | } 105 | 106 | .zoom-icon { 107 | font-size: 16px; 108 | color: var(--colorText); 109 | padding-left: 5px; 110 | } 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vizceral-example", 3 | "version": "4.3.0", 4 | "description": "Vizceral example app", 5 | "author": "Justin Reynolds ", 6 | "scripts": { 7 | "clean": "rimraf dist && mkdirp dist", 8 | "lint": "eslint src --ext .js --ext .jsx --fix", 9 | "copy:fonts": "cpx \"./node_modules/source-sans-pro/**/*\" ./dist/fonts", 10 | "copy:json": "cpx \"./src/*.json\" ./dist", 11 | "dev-server": "webpack-dev-server --host=0.0.0.0 --content-base dist/ --history-api-fallback --color", 12 | "dev": "npm-run-all clean --parallel copy:* dev-server", 13 | "validate": "npm ls" 14 | }, 15 | "engines": { 16 | "node": "~4.4.0" 17 | }, 18 | "license": "Apache-2.0", 19 | "dependencies": { 20 | "@tweenjs/tween.js": "^16.8.0", 21 | "bootstrap": "^3.3.7", 22 | "flux": "^3.1.0", 23 | "hammerjs": "^2.0.8", 24 | "jquery": "^3.1.1", 25 | "keymirror": "^0.1.1", 26 | "keypress.js": "2.1.0-1", 27 | "lodash": "^4.17.11", 28 | "numeral": "^1.5.3", 29 | "prop-types": "^15.7.2", 30 | "query-string": "^4.2.3", 31 | "react": "^16.8.6", 32 | "react-bootstrap": "^0.32.4", 33 | "react-dom": "^16.8.6", 34 | "react-virtualized": "^9.21.0", 35 | "socket.io-client": "^1.5.1", 36 | "source-sans-pro": "^2.0.10", 37 | "superagent": "^3.0.0", 38 | "vizceral-react": "^4.8.0", 39 | "webfontloader": "^1.6.26" 40 | }, 41 | "devDependencies": { 42 | "babel-core": "^6.18.0", 43 | "babel-eslint": "^7.1.0", 44 | "babel-loader": "^7.1.5", 45 | "babel-preset-env": "^1.7.0", 46 | "babel-preset-react": "^6.16.0", 47 | "babel-preset-stage-0": "^6.16.0", 48 | "cpx": "^1.5.0", 49 | "css-loader": "^1.0.0", 50 | "eslint": "^5.4.0", 51 | "eslint-config-airbnb-base": "^13.1.0", 52 | "eslint-loader": "^2.1.0", 53 | "eslint-plugin-import": "^2.0.1", 54 | "eslint-plugin-jsx-a11y": "^6.1.1", 55 | "eslint-plugin-react": "^7.11.1", 56 | "file-loader": "^2.0.0", 57 | "html-loader": "^0.5.5", 58 | "html-webpack-plugin": "^3.2.0", 59 | "mkdirp": "^0.5.1", 60 | "npm-run-all": "^4.1.3", 61 | "precommit-hook": "^3.0.0", 62 | "rimraf": "^2.5.4", 63 | "style-loader": "^0.23.0", 64 | "url-loader": "^1.1.1", 65 | "webpack": "^4.36.1", 66 | "webpack-cli": "^3.3.6", 67 | "webpack-dev-server": "^3.7.2" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "git@github.com:Netflix/vizceral-example.git" 72 | }, 73 | "pre-commit": [ 74 | "lint", 75 | "test" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /src/components/optionsPanel.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import './optionsPanel.css'; 7 | 8 | class OptionsPanel extends React.Component { 9 | constructor (props) { 10 | super(props); 11 | this.state = { 12 | showOptions: false, 13 | alignRight: false 14 | }; 15 | } 16 | 17 | componentDidMount () { 18 | this.setState({ alignRight: this.shouldAlignRight() }); 19 | } 20 | 21 | shouldAlignRight () { 22 | const elm = this.refs.optionsPanel; 23 | const boundingRect = elm.getBoundingClientRect(); 24 | const panelBoundingRect = elm.children[1].getBoundingClientRect(); 25 | 26 | const isEntirelyVisible = (boundingRect.left + panelBoundingRect.width <= window.innerWidth); 27 | return !isEntirelyVisible; 28 | } 29 | 30 | clearDocumentClick () { 31 | if (this.documentClickHandler) { 32 | document.removeEventListener('click', this.documentClickHandler, false); 33 | this.documentClickHandler = undefined; 34 | } 35 | } 36 | 37 | setDocumentClick () { 38 | this.clearDocumentClick(); 39 | this.documentClickHandler = this.handleDocumentClick.bind(this); 40 | document.addEventListener('click', this.documentClickHandler, false); 41 | } 42 | 43 | optionsDropdownClicked () { 44 | this.setState({ 45 | showOptions: !this.state.showOptions, 46 | alignRight: this.shouldAlignRight() 47 | }); 48 | } 49 | 50 | handleDocumentClick () { 51 | this.setState({ showOptions: false }); 52 | } 53 | 54 | componentWillUpdate (nextProps, nextState) { 55 | if (nextState.showOptions !== this.state.showOptions) { 56 | if (nextState.showOptions) { 57 | this.setDocumentClick(); 58 | } else { 59 | this.clearDocumentClick(); 60 | } 61 | } 62 | } 63 | 64 | handleClick (event) { 65 | event.stopPropagation(); 66 | event.nativeEvent.stopImmediatePropagation(); 67 | } 68 | 69 | render () { 70 | const panelStyles = { 71 | visibility: !this.state.showOptions ? 'hidden' : undefined, 72 | border: '1px solid grey' 73 | }; 74 | 75 | if (this.state.alignRight) { 76 | panelStyles.right = 0; 77 | } 78 | 79 | return ( 80 |
81 | 87 |
88 | { this.props.children } 89 |
90 |
91 | ); 92 | } 93 | } 94 | 95 | OptionsPanel.propTypes = { 96 | title: PropTypes.string.isRequired 97 | }; 98 | 99 | export default OptionsPanel; 100 | -------------------------------------------------------------------------------- /src/components/updateStatus.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import { Tooltip, OverlayTrigger } from 'react-bootstrap'; 7 | 8 | import './updateStatus.css'; 9 | 10 | function msToTimeAgo (ms) { 11 | if (!ms) { return 'Unknown'; } 12 | const secs = Math.round(ms / 1000); 13 | const hours = Math.floor(secs / (60 * 60)); 14 | 15 | const dM = secs % (60 * 60); 16 | const minutes = Math.floor(dM / 60); 17 | 18 | const dS = dM % 60; 19 | const seconds = Math.ceil(dS); 20 | 21 | const timeAgo = []; 22 | if (hours > 0) { 23 | timeAgo.push(`${hours}h`); 24 | } 25 | if (hours > 0 || minutes > 0) { 26 | timeAgo.push(`${minutes}m`); 27 | } 28 | timeAgo.push(`${seconds}s`); 29 | // return { h: hours, m: minutes, s: seconds }; 30 | return timeAgo.join(':'); 31 | } 32 | 33 | class UpdateStatus extends React.Component { 34 | constructor (props) { 35 | super(props); 36 | this.state = { 37 | status: this.updateFreshness(props.status) 38 | }; 39 | } 40 | 41 | updateFreshness (status) { 42 | const currentTime = Date.now() - this.props.baseOffset; 43 | _.each(status, (s) => { 44 | s.fresh = currentTime - (s.updated || 0) < this.props.warnThreshold; 45 | }); 46 | return status; 47 | } 48 | 49 | componentDidMount () { 50 | this.timer = setInterval(this.update.bind(this), 100); 51 | } 52 | 53 | componentWillUnmount () { 54 | clearInterval(this.timer); 55 | } 56 | 57 | update () { 58 | this.setState({ status: this.updateFreshness(this.props.status) }); 59 | } 60 | 61 | render () { 62 | const freshData = _.every(this.state.status, status => status.fresh); 63 | const glyphClass = `glyphicon glyphicon-refresh ${freshData ? 'severity0' : 'severity1'}`; 64 | const now = Date.now(); 65 | 66 | const tooltip = ( 67 | 68 |

Data last refreshed

69 | 70 | 71 | { 72 | this.props.status.map(status => ( 73 | 74 | 75 | 76 | 77 | )) 78 | } 79 | 80 |
{status.region}:{ msToTimeAgo(now - status.updated) }
81 |
82 | ); 83 | return ( 84 |
85 | 86 | 87 | 88 |
89 | ); 90 | } 91 | } 92 | 93 | UpdateStatus.propTypes = { 94 | status: PropTypes.arrayOf(PropTypes.shape({ 95 | region: PropTypes.string, 96 | updated: PropTypes.number 97 | })), 98 | warnThreshold: PropTypes.number, 99 | baseOffset: PropTypes.number 100 | }; 101 | 102 | export default UpdateStatus; 103 | -------------------------------------------------------------------------------- /src/components/detailsPanelNode.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import ConnectionList from './connectionList'; 7 | import DetailsSubpanel from './detailsSubpanel'; 8 | import DetailsSubpanelSubNodes from './detailsSubpanelSubNodes'; 9 | import Notices from './notices'; 10 | 11 | import './detailsPanel.css'; 12 | 13 | class DetailsPanelNode extends React.Component { 14 | constructor (props) { 15 | super(props); 16 | this.state = { 17 | node: props.node, 18 | region: props.region, 19 | description: undefined 20 | }; 21 | } 22 | 23 | componentWillReceiveProps (nextProps) { 24 | const newState = { 25 | region: nextProps.region, 26 | node: nextProps.node 27 | }; 28 | 29 | if (this.state.region !== nextProps.region || this.state.node.getName() !== nextProps.node.getName()) { 30 | newState.description = undefined; 31 | } 32 | 33 | this.setState(newState); 34 | } 35 | 36 | render () { 37 | const { node } = this.state; 38 | const isZoomOut = node.graphRenderer === 'focused'; 39 | const notices = (node && node.notices) || []; 40 | let zoomClassName = 'glyphicon clickable zoom-icon '; 41 | zoomClassName += isZoomOut ? 'glyphicon-log-out' : 'glyphicon-log-in'; 42 | const zoomTitle = `Zoom ${isZoomOut ? 'out of' : 'into'} node view`; 43 | 44 | return ( 45 |
46 |
47 |
{node.getName()} 48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 | { node && !node.isEntryNode() && node.nodes 56 | ? 57 | : undefined } 58 | { node && !node.isEntryNode() 59 | ? 60 | this.props.nodeClicked(clickedNode)} /> 61 | 62 | : undefined } 63 | 64 | this.props.nodeClicked(clickedNode)} /> 65 | 66 |
67 | ); 68 | } 69 | } 70 | 71 | DetailsPanelNode.propTypes = { 72 | closeCallback: PropTypes.func.isRequired, 73 | zoomCallback: PropTypes.func.isRequired, 74 | node: PropTypes.object.isRequired, 75 | nodeClicked: PropTypes.func, 76 | region: PropTypes.string 77 | }; 78 | 79 | DetailsPanelNode.defaultProps = { 80 | nodeClicked: () => {}, 81 | region: '' 82 | }; 83 | 84 | export default DetailsPanelNode; 85 | -------------------------------------------------------------------------------- /src/components/filterStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | import EventEmitter from 'events'; 5 | 6 | import AppDispatcher from '../appDispatcher'; 7 | import AppConstants from '../appConstants'; 8 | 9 | const CHANGE_EVENT = 'change'; 10 | 11 | const defaultFilters = { 12 | rps: { value: -1 } 13 | }; 14 | 15 | const noFilters = { 16 | rps: { value: -1 } 17 | }; 18 | 19 | const store = { 20 | filters: { 21 | rps: { 22 | name: 'rps', 23 | type: 'connection', 24 | passes: (object, value) => object.volumeTotal >= value, 25 | value: -1 26 | } 27 | }, 28 | states: { 29 | rps: [ 30 | { 31 | name: 'high', 32 | value: 1000 33 | }, 34 | { 35 | name: ' ', 36 | value: 300 37 | }, 38 | { 39 | name: ' ', 40 | value: 5 41 | }, 42 | { 43 | name: 'all', 44 | value: -1 45 | } 46 | ] 47 | } 48 | }; 49 | 50 | const resetDefaults = function () { 51 | _.merge(store.filters, defaultFilters); 52 | }; 53 | 54 | const clearFilters = function () { 55 | _.merge(store.filters, noFilters); 56 | }; 57 | 58 | resetDefaults(); 59 | 60 | class FilterStore extends EventEmitter { 61 | constructor () { 62 | super(); 63 | this.requests = {}; 64 | 65 | AppDispatcher.register((payload) => { 66 | const { action } = payload; 67 | switch (action.actionType) { 68 | case AppConstants.ActionTypes.UPDATE_FILTER: 69 | this.updateFilters(action.data); 70 | this.emit(CHANGE_EVENT); 71 | break; 72 | case AppConstants.ActionTypes.RESET_FILTERS: 73 | resetDefaults(); 74 | this.emit(CHANGE_EVENT); 75 | break; 76 | case AppConstants.ActionTypes.CLEAR_FILTERS: 77 | clearFilters(); 78 | this.emit(CHANGE_EVENT); 79 | break; 80 | default: 81 | return true; 82 | } 83 | return true; 84 | }); 85 | } 86 | 87 | addChangeListener (cb) { 88 | this.on(CHANGE_EVENT, cb); 89 | } 90 | 91 | removeChangeListener (cb) { 92 | this.removeListener(CHANGE_EVENT, cb); 93 | } 94 | 95 | getFilters () { 96 | return store.filters; 97 | } 98 | 99 | getFiltersArray () { 100 | return _.map(store.filters, filter => _.clone(filter)); 101 | } 102 | 103 | getStates () { 104 | return store.states; 105 | } 106 | 107 | getChangedFilters () { 108 | return _.filter(store.filters, filter => filter.value !== defaultFilters[filter.name].value); 109 | } 110 | 111 | getStepFromValue (name) { 112 | const index = _.findIndex(store.states[name], step => step.value === store.filters[name].value); 113 | if (index === -1) { 114 | return _.findIndex(store.states[name], step => step.value === defaultFilters[name].value); 115 | } 116 | return index; 117 | } 118 | 119 | updateFilters (filters) { 120 | Object.keys(filters).forEach((filter) => { 121 | store.filters[filter].value = filters[filter]; 122 | }); 123 | } 124 | 125 | isDefault () { 126 | return _.every(store.filters, filter => filter.value === defaultFilters[filter.name].value); 127 | } 128 | 129 | isClear () { 130 | return _.every(store.filters, filter => filter.value === noFilters[filter.name].value); 131 | } 132 | } 133 | 134 | const filterStore = new FilterStore(); 135 | 136 | export default filterStore; 137 | -------------------------------------------------------------------------------- /src/components/physicsOptions.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import './physicsOptions.css'; 7 | 8 | class PhysicsOptions extends React.Component { 9 | constructor (props) { 10 | super(props); 11 | this.state = { 12 | isEnabled: true, 13 | jaspersReplusionBetweenParticles: true, 14 | viscousDragCoefficient: 0.2, 15 | hooksSprings: { 16 | restLength: 50, 17 | springConstant: 0.2, 18 | dampingConstant: 0.1 19 | }, 20 | particles: { 21 | mass: 1 22 | } 23 | }; 24 | } 25 | 26 | componentWillReceiveProps (nextProps) { 27 | this.setState(nextProps.options); 28 | } 29 | 30 | changeState (newState) { 31 | this.setState(newState); 32 | this.props.changedCallback(newState); 33 | } 34 | 35 | render () { 36 | const { isEnabled, jaspersReplusionBetweenParticles } = this.state; 37 | return ( 38 |
39 |
40 | this.changeState({ isEnabled: !isEnabled })}/> 41 | 42 |
43 |
44 | 45 | this.changeState({ hooksSprings: { restLength: event.currentTarget.valueAsNumber } })}/> 46 |
47 |
48 | 49 | this.changeState({ hooksSprings: { springConstant: event.currentTarget.valueAsNumber } })}/> 50 |
51 |
52 | 53 | this.changeState({ hooksSprings: { dampingConstant: event.currentTarget.valueAsNumber } })}/> 54 |
55 |
56 | 57 | this.changeState({ viscousDragCoefficient: event.currentTarget.valueAsNumber })}/> 58 |
59 |
60 | 61 | this.changeState({ particles: { mass: event.currentTarget.valueAsNumber } })}/> 62 |
63 |
64 | this.changeState({ jaspersReplusionBetweenParticles: !jaspersReplusionBetweenParticles })}/> 65 | 66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | PhysicsOptions.propTypes = { 73 | options: PropTypes.object, 74 | changedCallback: PropTypes.func.isRequired 75 | }; 76 | 77 | export default PhysicsOptions; 78 | -------------------------------------------------------------------------------- /src/components/connectionList.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import numeral from 'numeral'; 7 | import { Table, Column, SortDirection } from 'react-virtualized'; 8 | import 'react-virtualized/styles.css'; 9 | 10 | const nameRenderer = function (data) { 11 | let className = 'glyphicon glyphicon-warning-sign'; 12 | const mostSevereNotice = data.rowData.notices && data.rowData.notices.length > 0 && _.maxBy(data.rowData.notices, notice => notice.severity); 13 | if (mostSevereNotice) { 14 | className += ` severity${mostSevereNotice.severity}`; 15 | } 16 | 17 | const styles = { 18 | paddingLeft: '5px', 19 | opacity: data.rowData.disabled ? 0.3 : undefined 20 | }; 21 | 22 | return ( 23 | 24 | {data.cellData} 25 | { 26 | mostSevereNotice 27 | ? 28 | : undefined 29 | } 30 | ); 31 | }; 32 | 33 | const errorRenderer = function (data) { 34 | return ( 35 | {numeral(data.rowData.errorRate || 0).format('0.[00]%')} 36 | ); 37 | }; 38 | 39 | const sorters = { 40 | name: (a, b) => { 41 | if (a.disabled && !b.disabled) { return 1; } 42 | if (!a.disabled && b.disabled) { return -1; } 43 | if (a.name < b.name) { return 1; } 44 | if (a.name > b.name) { return -1; } 45 | return 0; 46 | }, 47 | errorRate: (a, b) => { 48 | if (a.disabled && !b.disabled) { return 1; } 49 | if (!a.disabled && b.disabled) { return -1; } 50 | if (a.errorRate < b.errorRate) { return 1; } 51 | if (a.errorRate > b.errorRate) { return -1; } 52 | return 0; 53 | } 54 | }; 55 | 56 | class ConnectionList extends React.Component { 57 | constructor (props) { 58 | super(props); 59 | this.state = { 60 | connections: props.connections, 61 | sortBy: 'errorRate', 62 | sortDirection: SortDirection.ASC 63 | }; 64 | } 65 | 66 | componentWillReceiveProps (nextProps) { 67 | this.setState({ 68 | connections: nextProps.connections 69 | }); 70 | } 71 | 72 | render () { 73 | const headerHeight = 30; 74 | let estimatedRowHeight = 25; 75 | const maxTableHeight = 300; 76 | const connectionRows = this.state.connections.map((connection) => { 77 | const errors = connection.getVolume('danger'); 78 | const total = connection.getVolumeTotal(); 79 | const disabled = !connection.isVisible(); 80 | 81 | const classNames = []; 82 | if (disabled) { 83 | classNames.push('disabled'); 84 | } else if (connection.class) { 85 | classNames.push(`color-${connection.class}`); 86 | } 87 | 88 | return { 89 | name: this.props.direction === 'incoming' ? connection.source.getName() : connection.target.getName(), 90 | errorRate: errors / total || 0, 91 | errors: errors, 92 | total: total, 93 | className: classNames.join(' '), 94 | disabled: disabled, 95 | notices: connection.notices 96 | }; 97 | }); 98 | 99 | connectionRows.sort(sorters[this.state.sortBy]); 100 | if (this.state.sortDirection !== SortDirection.ASC) { _.reverse(connectionRows); } 101 | 102 | if (this.refs.flexTable && this.refs.flexTable.props.estimatedRowSize) { 103 | estimatedRowHeight = this.refs.flexTable.props.estimatedRowSize - 4; 104 | } 105 | const tableHeight = Math.min(maxTableHeight, (estimatedRowHeight * connectionRows.length) + headerHeight); 106 | 107 | 108 | return ( 109 | connectionRows.length > 0 110 | ?
111 | connectionRows[index]} 119 | sortBy={this.state.sortBy} 120 | sortDirection={this.state.sortDirection} 121 | sort={this.sort} 122 | > 123 | 124 | 125 |
126 |
127 | : None. 128 | ); 129 | } 130 | 131 | sort = ({ sortBy, sortDirection }) => { 132 | this.setState({ sortBy, sortDirection }); 133 | } 134 | } 135 | 136 | ConnectionList.propTypes = { 137 | direction: PropTypes.string.isRequired, 138 | connections: PropTypes.array.isRequired 139 | }; 140 | 141 | export default ConnectionList; 142 | -------------------------------------------------------------------------------- /src/components/subNodeList.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import { OverlayTrigger, Popover } from 'react-bootstrap'; 7 | import { Table, Column, SortDirection } from 'react-virtualized'; 8 | import numeral from 'numeral'; 9 | import 'react-virtualized/styles.css'; 10 | 11 | const nameRenderer = function (data) { 12 | return ({data.rowData.name}); 13 | }; 14 | 15 | const errorRenderer = function (data) { 16 | return ( 17 | {numeral(data.rowData.errorRate || 0).format('0.[00]%')} 18 | ); 19 | }; 20 | 21 | const totalRenderer = function (data) { 22 | return ( 23 | {numeral(data.rowData.totalPercent || 0).format('0.[00]%')} 24 | ); 25 | }; 26 | 27 | const sorters = { 28 | name: (a, b) => { 29 | if (a.disabled && !b.disabled) { return 1; } 30 | if (!a.disabled && b.disabled) { return -1; } 31 | if (a.name < b.name) { return 1; } 32 | if (a.name > b.name) { return -1; } 33 | return 0; 34 | }, 35 | errorRate: (a, b) => { 36 | if (a.disabled && !b.disabled) { return 1; } 37 | if (!a.disabled && b.disabled) { return -1; } 38 | if (a.errorRate < b.errorRate) { return 1; } 39 | if (a.errorRate > b.errorRate) { return -1; } 40 | return 0; 41 | }, 42 | totalPercent: (a, b) => { 43 | if (a.disabled && !b.disabled) { return 1; } 44 | if (!a.disabled && b.disabled) { return -1; } 45 | if (a.total < b.total) { return 1; } 46 | if (a.total > b.total) { return -1; } 47 | return 0; 48 | } 49 | }; 50 | 51 | class SubNodeList extends React.Component { 52 | constructor (props) { 53 | super(props); 54 | this.state = { 55 | nodes: props.nodes, 56 | sortBy: 'totalPercent', 57 | sortDirection: SortDirection.ASC 58 | }; 59 | 60 | this.linkPopover = (node) => { 61 | const atlasBackend = this.props.region ? `&backend=http:%2F%2Fatlas-main.${this.props.region}.prod.netflix.net:7001` : ''; 62 | const atlasLink = `http://atlasui.prod.netflix.net/ui/graph?g.q=nf.cluster,${node.name},:eq,name,RequestStats-all-requests-_Num,:re,:and,:sum,(,name,),:by&g.e=now-1m&g.s=e-3h&g.tz=US%2FPacific&mode=png&vsplit=520px&sel=expr.0.0${atlasBackend}`; 63 | const spinnakerRegion = this.props.region ? `®=${this.props.region}` : ''; 64 | const spinnakerLink = `https://spinnaker.prod.netflix.net/#/applications/${node.app}/clusters?acct=prod&q=cluster:${node.name}${spinnakerRegion}`; 65 | return ( 66 | 67 |
OPEN IN
68 | 69 | 70 |
71 | ); 72 | }; 73 | 74 | 75 | this.linkRenderer = data => (data.rowData.app ? ( 76 | 77 |
78 |
79 | ) : undefined); 80 | } 81 | 82 | componentWillReceiveProps (nextProps) { 83 | this.setState({ 84 | nodes: nextProps.nodes 85 | }); 86 | } 87 | 88 | render () { 89 | const headerHeight = 30; 90 | let estimatedRowHeight = 25; 91 | const maxTableHeight = 300; 92 | const trafficTotal = _.sum(_.flatten(_.map(this.state.nodes, c => _.values(c.metrics)))); 93 | const nodeRows = _.map(this.state.nodes, (node) => { 94 | const total = _.sum(_.values(node.metrics)); 95 | const totalPercent = total / trafficTotal; 96 | const errors = (node.metrics && node.metrics.danger) || 0; 97 | const errorRate = errors / total; 98 | let colorName; 99 | if (errorRate > 0.1) { 100 | colorName = 'danger'; 101 | } else if (errorRate > 0.03) { 102 | colorName = 'warning'; 103 | } 104 | const className = colorName ? `color-${colorName}` : ''; 105 | 106 | return { 107 | name: node.name, 108 | errors: errors, 109 | total: total, 110 | errorRate: errorRate, 111 | totalPercent: totalPercent, 112 | app: node.app, 113 | className: className 114 | }; 115 | }); 116 | 117 | nodeRows.sort(sorters[this.state.sortBy]); 118 | if (this.state.sortDirection !== SortDirection.ASC) { _.reverse(nodeRows); } 119 | 120 | if (this.refs.flexTable && this.refs.flexTable.props.estimatedRowSize) { 121 | estimatedRowHeight = this.refs.flexTable.props.estimatedRowSize - 4; 122 | } 123 | const tableHeight = Math.min(maxTableHeight, (estimatedRowHeight * nodeRows.length) + headerHeight); 124 | 125 | 126 | return ( 127 | nodeRows.length > 0 128 | ?
129 | nodeRows[index]} 137 | sortBy={this.state.sortBy} 138 | sortDirection={this.state.sortDirection} 139 | sort={this.sort} 140 | > 141 | 142 | 143 | 144 | 145 |
146 |
147 | : None. 148 | ); 149 | } 150 | 151 | sort = ({ sortBy, sortDirection }) => { 152 | this.setState({ sortBy, sortDirection }); 153 | } 154 | } 155 | 156 | SubNodeList.propTypes = { 157 | nodes: PropTypes.array.isRequired, 158 | region: PropTypes.string 159 | }; 160 | 161 | export default SubNodeList; 162 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | min-height: 100%; 3 | height:100%; 4 | } 5 | body { 6 | position: relative; 7 | background-color: var(--colorPageBackground); 8 | color: var(--colorText); 9 | font-family: 'Source Sans Pro', sans-serif; 10 | font-weight: 400; 11 | font-style: normal; 12 | height: 100%; 13 | } 14 | 15 | .color-normal, .severity0 { 16 | color: var(--colorNormal); 17 | } 18 | 19 | .color-warning, .severity1 { 20 | color: var(--colorWarning); 21 | } 22 | 23 | .color-danger, .severity2 { 24 | color: var(--colorDanger); 25 | } 26 | 27 | .disabled { 28 | color: var(--colorTextDisabled); 29 | } 30 | 31 | /* Custom page CSS 32 | -------------------------------------------------- */ 33 | body > .container { 34 | position: absolute; 35 | top: 51px; 36 | padding-left: 0; 37 | padding-right: 0; 38 | margin-left: 0; 39 | margin-right: 0; 40 | bottom: 0; 41 | left: 0; 42 | right: 0; 43 | width: 100%; 44 | } 45 | 46 | .container .text-muted { 47 | margin: 20px 0; 48 | } 49 | 50 | .navbar { 51 | background-color: var(--colorNormalDonut); 52 | } 53 | 54 | .navbar-brand { 55 | color: #fff !important; 56 | font-weight: 200; 57 | text-shadow: 0 1px 1px #333; 58 | } 59 | 60 | .navbar-text { 61 | color: #fff !important; 62 | font-weight: 200; 63 | } 64 | 65 | /* Feedback 66 | -------------------------------------------------- */ 67 | .glyphicon-comment { 68 | top: 3px; 69 | padding-right: 5px; 70 | } 71 | 72 | #feedback { 73 | padding-right: 25px; 74 | } 75 | 76 | /* Traffic 77 | -------------------------------------------------- */ 78 | .vizceral-container { 79 | height: 100%; 80 | position: relative; 81 | } 82 | 83 | .service-traffic-map { 84 | position: absolute; 85 | top: 87px; 86 | bottom: 0; 87 | left: 0; 88 | right: 0; 89 | } 90 | 91 | .hidden { 92 | display: none; 93 | } 94 | 95 | .clickable { 96 | cursor: pointer; 97 | } 98 | 99 | :root, .vizceral { 100 | --colorText: rgb(214, 214, 214); 101 | --colorTextDisabled: rgb(129, 129, 129); 102 | --colorNormal: rgb(186, 213, 237); 103 | --colorWarning: rgb(268, 185, 73); 104 | --colorDanger: rgb(184, 36, 36); 105 | --colorNormalDimmed: rgb(101, 117, 128); 106 | --colorBackgroundDark: rgb(35, 35, 35); 107 | --colorNormalDonut: rgb(91, 91, 91); 108 | --colorLabelBorder: rgb(16, 17, 18); 109 | --colorLabelText: rgb(0, 0, 0); 110 | --colorDonutInternalColor: rgb(35, 35, 35); 111 | --colorDonutInternalColorHighlighted: rgb(255, 255, 255); 112 | --colorConnectionLine: rgb(91, 91, 91); 113 | --colorPageBackground: rgb(45, 45, 45); 114 | --colorPageBackgroundTransparent: rgba(45, 45, 45, 0); 115 | --colorBorderLines: rgb(137, 137, 137); 116 | } 117 | 118 | /* Subheader 119 | -------------------------------------------------- */ 120 | .subheader { 121 | padding: 5px 15px 0 15px; 122 | color: var(--colorBorderLines); 123 | height: 45px; 124 | border-bottom: 1px solid var(--colorBorderLines); 125 | } 126 | 127 | .subheader > div { 128 | display: inline-block; 129 | } 130 | 131 | .date-time-slider-container { 132 | height: 45px; 133 | } 134 | 135 | /* STEPPER */ 136 | .stepper { 137 | list-style: none; 138 | margin: 0; 139 | padding: 0; 140 | display: table; 141 | table-layout: fixed; 142 | width: 100%; 143 | color: var(--colorNormalDonut); 144 | } 145 | .stepper > li { 146 | position: relative; 147 | display: table-cell; 148 | text-align: center; 149 | font-size: 0.8em; 150 | cursor: pointer; 151 | } 152 | .stepper > li:before { 153 | content: attr(data-step); 154 | display: block; 155 | margin: 0 auto; 156 | background: var(--colorNormalDonut); 157 | width: 1.3em; 158 | height: 1.3em; 159 | text-align: center; 160 | margin-bottom: 0.25em; 161 | line-height: 1.3em; 162 | border-radius: 100%; 163 | position: relative; 164 | z-index: 1000; 165 | } 166 | .stepper > li:after { 167 | content: ''; 168 | position: absolute; 169 | display: block; 170 | background: var(--colorNormalDonut); 171 | width: 100%; 172 | height: 0.4em; 173 | top: 0.45em; 174 | left: 50%; 175 | z-index: -1; 176 | } 177 | .stepper > li:last-child:after { 178 | display: none; 179 | } 180 | .stepper > li.is-lower { 181 | color: var(--colorNormal); 182 | } 183 | .stepper > li.is-lower:before { 184 | color: #FFF; 185 | background: var(--colorNormal); 186 | } 187 | .stepper > li.is-lower:after { 188 | color: #FFF; 189 | } 190 | .stepper > li.show-bar:after { 191 | background: var(--colorNormal); 192 | } 193 | .stepper > li.is-current { 194 | color: var(--colorNormal); 195 | } 196 | .stepper > li.is-current:before { 197 | color: #FFF; 198 | background: var(--colorNormal); 199 | } 200 | /* END STEPPER */ 201 | 202 | .subsection { 203 | padding-left: 15px; 204 | padding-right: 15px; 205 | padding-bottom: 10px; 206 | } 207 | 208 | .panel-default > .panel-heading { 209 | background-color: rgb(87, 87, 87); 210 | color: var(--colorText); 211 | border-top-left-radius: 0; 212 | border-top-right-radius: 0; 213 | border-left-width: 0; 214 | } 215 | 216 | .panel-default > .panel-body, 217 | .panel-group .panel-heading + .panel-collapse > .panel-body { 218 | border-top-color: var(--colorBorderLines); 219 | padding-left: 17px; 220 | } 221 | 222 | .panel-body { 223 | padding: 0; 224 | } 225 | 226 | .panel-group { 227 | margin-bottom: 0; 228 | } 229 | 230 | .panel-group .panel { 231 | background-color: transparent; 232 | border-radius: 0; 233 | border-top: 1px solid var(--colorBorderLines); 234 | border-bottom: 0; 235 | border-left: 0; 236 | border-right: 0; 237 | } 238 | 239 | .panel-group:last-of-type .panel:last-of-type { 240 | border-bottom: 1px solid var(--colorBorderLines); 241 | } 242 | 243 | .ReactVirtualized__Table__headerColumn:first-of-type, .ReactVirtualized__Table__rowColumn:first-of-type { 244 | margin-left: 0 !important; 245 | } 246 | 247 | .ReactVirtualized__Table__headerColumn:focus, .ReactVirtualized__Table__Grid:focus { 248 | outline: none; 249 | } 250 | .ReactVirtualized__Table__row { 251 | border-bottom: 1px solid var(--colorBorderLines); 252 | } 253 | 254 | .ReactVirtualized__Table__headerColumn:last-of-type, .ReactVirtualized__Table__rowColumn:last-of-type { 255 | margin-right: 0 !important; 256 | } 257 | 258 | .ReactVirtualized__Table__headerRow { 259 | text-transform: none !important; 260 | border-bottom: 2px solid; 261 | } 262 | 263 | a.accordion-toggle { 264 | text-decoration: none; 265 | } 266 | 267 | /* SPINNER */ 268 | @keyframes spinner { 269 | to {transform: rotate(360deg);} 270 | } 271 | 272 | @-webkit-keyframes spinner { 273 | to {-webkit-transform: rotate(360deg);} 274 | } 275 | 276 | .inline-spinner { 277 | min-width: 30px; 278 | min-height: 2px; 279 | position: relative; 280 | display: inline-block; 281 | } 282 | 283 | .inline-spinner:after { 284 | content: 'Loading…'; 285 | position: absolute; 286 | top: 50%; 287 | left: 50%; 288 | width: 13px; 289 | height: 13px; 290 | margin-top: -10px; 291 | margin-left: -10px; 292 | } 293 | 294 | .inline-spinner:not(:required):after { 295 | content: ''; 296 | border-radius: 50%; 297 | border: 2px solid rgba(129, 129, 129, .3); 298 | border-top-color: rgba(129, 129, 129, .6); 299 | animation: spinner .6s linear infinite; 300 | -webkit-animation: spinner .6s linear infinite; 301 | -moz-animation: spinner .6s linear infinite; 302 | } 303 | /* END SPINNER */ 304 | 305 | .clear-link-styles, .clear-link-styles:hover, .clear-link-styles:active, .clear-link-styles:visited { 306 | color: inherit; 307 | text-decoration: inherit; 308 | } 309 | 310 | #links-popover .popover-content { 311 | color: rgb(172, 182, 187); 312 | text-align: right; 313 | width: 100px; 314 | } 315 | 316 | .links-popover-title { 317 | font-size: 10px; 318 | } 319 | 320 | .links-popover-link, .links-popover-link a, .links-popover-link a:active, .links-popover-link a:hover, .links-popover-link a:visited { 321 | color: rgb(110, 104, 99); 322 | cursor: pointer; 323 | font-size: 12px; 324 | text-decoration: none; 325 | } 326 | 327 | .popover { 328 | border-radius: 3px !important; 329 | font-family: 'Source Sans Pro', sans-serif !important; 330 | } 331 | 332 | .popover-content { 333 | padding: 6px 8px !important; 334 | } 335 | 336 | .table-borderless tbody tr td, .table-borderless tbody tr th, .table-borderless thead tr th { 337 | border: none; 338 | padding: 0; 339 | } 340 | 341 | .tooltip-inner { 342 | max-width: 250px; 343 | font-family: 'Source Sans Pro', sans-serif !important; 344 | line-height: 1.5em; 345 | } 346 | 347 | .tooltip-inner .table { 348 | margin-bottom: 0; 349 | } 350 | 351 | .tooltip-inner .header { 352 | margin: 0; 353 | font-weight: 700; 354 | } 355 | 356 | .alert { 357 | padding: 5px 10px; 358 | position: absolute; 359 | left: 100px; 360 | top: -42px; 361 | z-index: 99999; 362 | } 363 | 364 | .alert-info { 365 | color: #000000; 366 | background-color: var(--colorNormal); 367 | } 368 | 369 | .alert-dismissable .close { 370 | top: -1px; 371 | right: -5px; 372 | } 373 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016 Netflix, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/components/trafficFlow.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import _ from 'lodash'; 4 | import { Alert } from 'react-bootstrap'; 5 | import React from 'react'; 6 | import TWEEN from '@tweenjs/tween.js'; // Start TWEEN updates for sparklines and loading screen fading out 7 | import Vizceral from 'vizceral-react'; 8 | import 'vizceral-react/dist/vizceral.css'; 9 | import keypress from 'keypress.js'; 10 | import queryString from 'query-string'; 11 | import request from 'superagent'; 12 | 13 | import './trafficFlow.css'; 14 | import Breadcrumbs from './breadcrumbs'; 15 | import DisplayOptions from './displayOptions'; 16 | import PhysicsOptions from './physicsOptions'; 17 | import FilterControls from './filterControls'; 18 | import DetailsPanelConnection from './detailsPanelConnection'; 19 | import DetailsPanelNode from './detailsPanelNode'; 20 | import LoadingCover from './loadingCover'; 21 | import Locator from './locator'; 22 | import OptionsPanel from './optionsPanel'; 23 | import UpdateStatus from './updateStatus'; 24 | 25 | import filterActions from './filterActions'; 26 | import filterStore from './filterStore'; 27 | 28 | const listener = new keypress.Listener(); 29 | 30 | const hasOwnPropFunc = Object.prototype.hasOwnProperty; 31 | 32 | function animate (time) { 33 | requestAnimationFrame(animate); 34 | TWEEN.update(time); 35 | } 36 | requestAnimationFrame(animate); 37 | 38 | const panelWidth = 400; 39 | 40 | class TrafficFlow extends React.Component { 41 | constructor (props) { 42 | super(props); 43 | 44 | this.state = { 45 | currentView: undefined, 46 | redirectedFrom: undefined, 47 | selectedChart: undefined, 48 | displayOptions: { 49 | allowDraggingOfNodes: false, 50 | showLabels: true 51 | }, 52 | currentGraph_physicsOptions: { 53 | isEnabled: true, 54 | viscousDragCoefficient: 0.2, 55 | hooksSprings: { 56 | restLength: 50, 57 | springConstant: 0.2, 58 | dampingConstant: 0.1 59 | }, 60 | particles: { 61 | mass: 1 62 | } 63 | }, 64 | labelDimensions: {}, 65 | appliedFilters: filterStore.getChangedFilters(), 66 | filters: filterStore.getFiltersArray(), 67 | searchTerm: '', 68 | matches: { 69 | total: -1, 70 | visible: -1 71 | }, 72 | trafficData: { 73 | nodes: [], 74 | connections: [] 75 | }, 76 | regionUpdateStatus: [], 77 | timeOffset: 0, 78 | modes: { 79 | detailedNode: 'volume' 80 | } 81 | }; 82 | 83 | // Browser history support 84 | window.addEventListener('popstate', event => this.handlePopState(event.state)); 85 | 86 | // Keyboard interactivity 87 | listener.simple_combo('esc', () => { 88 | if (this.state.detailedNode) { 89 | this.setState({ detailedNode: undefined }); 90 | } else if (this.state.currentView.length > 0) { 91 | this.setState({ currentView: this.state.currentView.slice(0, -1) }); 92 | } 93 | }); 94 | } 95 | 96 | handlePopState () { 97 | const state = window.history.state || {}; 98 | this.poppedState = true; 99 | this.setState({ currentView: state.selected, objectToHighlight: state.highlighted }); 100 | } 101 | 102 | viewChanged = (data) => { 103 | const changedState = { 104 | currentView: data.view, 105 | searchTerm: '', 106 | matches: { total: -1, visible: -1 }, 107 | redirectedFrom: data.redirectedFrom 108 | }; 109 | if (hasOwnPropFunc.call(data, 'graph')) { 110 | let oldCurrentGraph = this.state.currentGraph; 111 | if (oldCurrentGraph == null) oldCurrentGraph = null; 112 | let newCurrentGraph = data.graph; 113 | if (newCurrentGraph == null) newCurrentGraph = null; 114 | if (oldCurrentGraph !== newCurrentGraph) { 115 | changedState.currentGraph = newCurrentGraph; 116 | const o = newCurrentGraph === null ? null : newCurrentGraph.getPhysicsOptions(); 117 | changedState.currentGraph_physicsOptions = o; 118 | } 119 | } 120 | this.setState(changedState); 121 | } 122 | 123 | viewUpdated = () => { 124 | this.setState({}); 125 | } 126 | 127 | objectHighlighted = (highlightedObject) => { 128 | // need to set objectToHighlight for diffing on the react component. since it was already highlighted here, it will be a noop 129 | this.setState({ 130 | highlightedObject: highlightedObject, objectToHighlight: highlightedObject ? highlightedObject.getName() : undefined, searchTerm: '', matches: { total: -1, visible: -1 }, redirectedFrom: undefined 131 | }); 132 | } 133 | 134 | nodeContextSizeChanged = (dimensions) => { 135 | this.setState({ labelDimensions: dimensions }); 136 | } 137 | 138 | checkInitialRoute () { 139 | // Check the location bar for any direct routing information 140 | const pathArray = window.location.pathname.split('/'); 141 | const currentView = []; 142 | if (pathArray[1]) { 143 | currentView.push(pathArray[1]); 144 | if (pathArray[2]) { 145 | currentView.push(pathArray[2]); 146 | } 147 | } 148 | const parsedQuery = queryString.parse(window.location.search); 149 | 150 | this.setState({ currentView: currentView, objectToHighlight: parsedQuery.highlighted }); 151 | } 152 | 153 | beginSampleData () { 154 | this.traffic = { nodes: [], connections: [] }; 155 | request.get('sample_data.json') 156 | .set('Accept', 'application/json') 157 | .end((err, res) => { 158 | if (res && res.status === 200) { 159 | this.traffic.clientUpdateTime = Date.now(); 160 | this.updateData(res.body); 161 | } 162 | }); 163 | } 164 | 165 | componentDidMount () { 166 | this.checkInitialRoute(); 167 | this.beginSampleData(); 168 | 169 | // Listen for changes to the stores 170 | filterStore.addChangeListener(this.filtersChanged); 171 | } 172 | 173 | componentWillUnmount () { 174 | filterStore.removeChangeListener(this.filtersChanged); 175 | } 176 | 177 | shouldComponentUpdate (nextProps, nextState) { 178 | if (!this.state.currentView 179 | || this.state.currentView[0] !== nextState.currentView[0] 180 | || this.state.currentView[1] !== nextState.currentView[1] 181 | || this.state.highlightedObject !== nextState.highlightedObject) { 182 | const titleArray = (nextState.currentView || []).slice(0); 183 | titleArray.unshift('Vizceral'); 184 | document.title = titleArray.join(' / '); 185 | 186 | if (this.poppedState) { 187 | this.poppedState = false; 188 | } else if (nextState.currentView) { 189 | const highlightedObjectName = nextState.highlightedObject && nextState.highlightedObject.getName(); 190 | const state = { 191 | title: document.title, 192 | url: `/${nextState.currentView.join('/')}${highlightedObjectName ? `?highlighted=${highlightedObjectName}` : ''}`, 193 | selected: nextState.currentView, 194 | highlighted: highlightedObjectName 195 | }; 196 | window.history.pushState(state, state.title, state.url); 197 | } 198 | } 199 | return true; 200 | } 201 | 202 | updateData (newTraffic) { 203 | const regionUpdateStatus = _.map(_.filter(newTraffic.nodes, n => n.name !== 'INTERNET'), node => ({ region: node.name, updated: node.updated })); 204 | const lastUpdatedTime = _.max(_.map(regionUpdateStatus, 'updated')); 205 | this.setState({ 206 | regionUpdateStatus: regionUpdateStatus, 207 | timeOffset: newTraffic.clientUpdateTime - newTraffic.serverUpdateTime, 208 | lastUpdatedTime: lastUpdatedTime, 209 | trafficData: newTraffic 210 | }); 211 | } 212 | 213 | zoomCallback = () => { 214 | const newState = { 215 | currentView: this.state.currentView.slice() 216 | }; 217 | 218 | if (this.state.highlightedObject) { 219 | // zooming in 220 | newState.currentView.push(this.state.highlightedObject.name); 221 | newState.objectToHighlight = undefined; 222 | } else if (newState.currentView.length > 0) { 223 | // zooming out 224 | const nodeName = newState.currentView.pop(); 225 | newState.objectToHighlight = nodeName; 226 | } 227 | 228 | this.setState(newState); 229 | } 230 | 231 | displayOptionsChanged = (options) => { 232 | const displayOptions = _.merge({}, this.state.displayOptions, options); 233 | this.setState({ displayOptions: displayOptions }); 234 | } 235 | 236 | physicsOptionsChanged = (physicsOptions) => { 237 | this.setState({ currentGraph_physicsOptions: physicsOptions }); 238 | let { currentGraph } = this.state; 239 | if (currentGraph == null) currentGraph = null; 240 | if (currentGraph !== null) { 241 | currentGraph.setPhysicsOptions(physicsOptions); 242 | } 243 | } 244 | 245 | navigationCallback = (newNavigationState) => { 246 | this.setState({ currentView: newNavigationState }); 247 | } 248 | 249 | detailsClosed = () => { 250 | const { currentGraph, currentView } = this.state; 251 | const newState = {}; 252 | // If the current graph type is a focused graph 253 | if (currentGraph.type === 'focused') { 254 | // If there is a parent graph, navigate to parent view. 255 | if (currentGraph.parentGraph) { 256 | if (currentView.length > 0) { 257 | const newView = Array.from(currentView); 258 | newView.pop(); 259 | newState.currentView = newView; 260 | } 261 | // If there is a parent graph that is _not_ a focused graph, close the panel 262 | if (currentGraph.parentGraph.type !== 'focused') { 263 | newState.focusedNode = undefined; 264 | newState.highlightedObject = undefined; 265 | } 266 | } 267 | } else { 268 | newState.focusedNode = undefined; 269 | newState.highlightedObject = undefined; 270 | } 271 | 272 | this.setState(newState); 273 | } 274 | 275 | filtersChanged = () => { 276 | this.setState({ 277 | appliedFilters: filterStore.getChangedFilters(), 278 | filters: filterStore.getFiltersArray() 279 | }); 280 | } 281 | 282 | filtersCleared = () => { 283 | if (!filterStore.isClear()) { 284 | if (!filterStore.isDefault()) { 285 | filterActions.resetFilters(); 286 | } else { 287 | filterActions.clearFilters(); 288 | } 289 | } 290 | } 291 | 292 | locatorChanged = (value) => { 293 | this.setState({ searchTerm: value }); 294 | } 295 | 296 | matchesFound = (matches) => { 297 | this.setState({ matches: matches }); 298 | } 299 | 300 | nodeClicked = (node) => { 301 | if (this.state.currentView.length === 1) { 302 | // highlight node 303 | this.setState({ objectToHighlight: node.getName() }); 304 | } else if (this.state.currentView.length === 2) { 305 | // detailed view of node 306 | this.setState({ currentView: [this.state.currentView[0], node.getName()] }); 307 | } 308 | } 309 | 310 | resetLayoutButtonClicked = () => { 311 | const g = this.state.currentGraph; 312 | if (g != null) { 313 | g._relayout(); 314 | } 315 | } 316 | 317 | dismissAlert = () => { 318 | this.setState({ redirectedFrom: undefined }); 319 | } 320 | 321 | render () { 322 | const { trafficData } = this.state; 323 | const focusedNode = this.state.currentGraph && this.state.currentGraph.focusedNode; 324 | const nodeToShowDetails = focusedNode || (this.state.highlightedObject && this.state.highlightedObject.type === 'node' ? this.state.highlightedObject : undefined); 325 | const connectionToShowDetails = this.state.highlightedObject && this.state.highlightedObject.type === 'connection' ? this.state.highlightedObject : undefined; 326 | const showLoadingCover = !this.state.currentGraph; 327 | const showBreadcrumbs = trafficData && trafficData.nodes && trafficData.nodes.length > 0; 328 | const breadcrumbsRoot = _.get(trafficData, 'name', 'root'); 329 | 330 | let matches; 331 | if (this.state.currentGraph) { 332 | matches = { 333 | totalMatches: this.state.matches.total, 334 | visibleMatches: this.state.matches.visible, 335 | total: this.state.currentGraph.nodeCounts.total, 336 | visible: this.state.currentGraph.nodeCounts.visible 337 | }; 338 | } 339 | 340 | return ( 341 |
342 | { this.state.redirectedFrom 343 | ? 344 | {this.state.redirectedFrom.join('/') || '/'} does not exist, you were redirected to {this.state.currentView.join('/') || '/'} instead 345 | 346 | : undefined } 347 |
348 | {showBreadcrumbs && } 349 | {showBreadcrumbs && } 350 |
351 | { (!focusedNode && matches) && } 352 | 353 | 354 | 355 | Reset Layout 356 |
357 |
358 |
359 |
362 | 376 |
377 | { 378 | !!nodeToShowDetails 379 | && this.nodeClicked(node)} 385 | /> 386 | } 387 | { 388 | !!connectionToShowDetails 389 | && this.nodeClicked(node)} 394 | /> 395 | } 396 | 397 |
398 |
399 | ); 400 | } 401 | } 402 | 403 | TrafficFlow.propTypes = { 404 | }; 405 | 406 | export default TrafficFlow; 407 | -------------------------------------------------------------------------------- /src/sample_data_region_only.json: -------------------------------------------------------------------------------- 1 | { 2 | "renderer": "region", 3 | "name": "us-east-1", 4 | "displayName": "US-EAST-1", 5 | "updated": 1477690448572, 6 | "nodes": [ 7 | { 8 | "name": "viraemic", 9 | "metadata": { 10 | "streaming": 1 11 | }, 12 | "nodes": [ 13 | { 14 | "name": "tetrabrach", 15 | "metrics": { 16 | "normal": 41515.944, 17 | "danger": 66.144 18 | } 19 | }, 20 | { 21 | "name": "colloidal", 22 | "metrics": { 23 | "danger": 0.166, 24 | "normal": 126.34000000000002 25 | } 26 | }, 27 | { 28 | "name": "wardrobers" 29 | }, 30 | { 31 | "name": "yplast" 32 | }, 33 | { 34 | "name": "benet", 35 | "metrics": { 36 | "danger": 0.22400000000000003, 37 | "normal": 130.17600000000002 38 | } 39 | }, 40 | { 41 | "name": "imping", 42 | "metrics": { 43 | "danger": 0.22000000000000003, 44 | "normal": 130.19000000000003 45 | } 46 | }, 47 | { 48 | "name": "virility", 49 | "metrics": { 50 | "danger": 0.18600000000000003, 51 | "normal": 130.158 52 | } 53 | }, 54 | { 55 | "name": "eng", 56 | "metrics": { 57 | "danger": 0.244, 58 | "normal": 129.494 59 | } 60 | }, 61 | { 62 | "name": "use", 63 | "metrics": { 64 | "danger": 0.2, 65 | "normal": 126.536 66 | } 67 | }, 68 | { 69 | "name": "racegoings", 70 | "metrics": { 71 | "danger": 0.06999999999999999, 72 | "normal": 43.19200000000001 73 | } 74 | } 75 | ], 76 | "renderer": "focusedChild" 77 | }, 78 | { 79 | "name": "foetal", 80 | "metadata": { 81 | "streaming": 1 82 | }, 83 | "nodes": [ 84 | { 85 | "name": "amperzands", 86 | "metrics": { 87 | "danger": 0.006, 88 | "normal": 18187.73 89 | } 90 | } 91 | ], 92 | "renderer": "focusedChild" 93 | }, 94 | { 95 | "name": "buglers", 96 | "metadata": { 97 | "streaming": 1 98 | }, 99 | "nodes": [ 100 | { 101 | "name": "blamer", 102 | "metrics": { 103 | "danger": 4.220000000000001, 104 | "normal": 12450.862000000001 105 | } 106 | } 107 | ], 108 | "renderer": "focusedChild" 109 | }, 110 | { 111 | "name": "lassoing", 112 | "metadata": { 113 | "streaming": 1 114 | }, 115 | "nodes": [ 116 | { 117 | "name": "jumbies", 118 | "metrics": { 119 | "danger": 9429.436, 120 | "normal": 124.54400000000001 121 | } 122 | } 123 | ], 124 | "renderer": "focusedChild" 125 | }, 126 | { 127 | "name": "proletarianised", 128 | "metadata": { 129 | "streaming": 1 130 | }, 131 | "nodes": [ 132 | { 133 | "name": "prefocuses", 134 | "metrics": { 135 | "danger": 1, 136 | "normal": 7877.054 137 | } 138 | } 139 | ], 140 | "renderer": "focusedChild" 141 | }, 142 | { 143 | "name": "neuritics", 144 | "metadata": { 145 | "streaming": 1 146 | }, 147 | "nodes": [ 148 | { 149 | "name": "difference", 150 | "metrics": { 151 | "normal": 6652.246000000001 152 | } 153 | } 154 | ], 155 | "renderer": "focusedChild" 156 | }, 157 | { 158 | "name": "disseat", 159 | "metadata": { 160 | "streaming": 1 161 | }, 162 | "nodes": [ 163 | { 164 | "name": "nymphae", 165 | "metrics": { 166 | "normal": 4804.088, 167 | "danger": 0.264 168 | } 169 | } 170 | ], 171 | "renderer": "focusedChild" 172 | }, 173 | { 174 | "name": "risks", 175 | "metadata": { 176 | "streaming": 1 177 | }, 178 | "nodes": [ 179 | { 180 | "name": "decreasingly", 181 | "metrics": { 182 | "normal": 3919.876 183 | } 184 | } 185 | ], 186 | "renderer": "focusedChild" 187 | }, 188 | { 189 | "name": "wickyups", 190 | "metadata": { 191 | "streaming": 1 192 | }, 193 | "nodes": [ 194 | { 195 | "name": "reformings", 196 | "metrics": { 197 | "danger": 0.036, 198 | "normal": 3147.704 199 | } 200 | } 201 | ], 202 | "renderer": "focusedChild" 203 | }, 204 | { 205 | "name": "disenthral", 206 | "metadata": { 207 | "streaming": 1 208 | }, 209 | "nodes": [ 210 | { 211 | "name": "unaligned", 212 | "metrics": { 213 | "normal": 3954.196 214 | } 215 | } 216 | ], 217 | "renderer": "focusedChild" 218 | }, 219 | { 220 | "name": "macrurous", 221 | "metadata": { 222 | "streaming": 1 223 | }, 224 | "nodes": [ 225 | { 226 | "name": "tsesarevich", 227 | "metrics": { 228 | "danger": 2095.25, 229 | "normal": 146.374 230 | } 231 | } 232 | ], 233 | "renderer": "focusedChild" 234 | }, 235 | { 236 | "name": "parrocks", 237 | "metadata": { 238 | "streaming": 1 239 | }, 240 | "nodes": [ 241 | { 242 | "name": "hoarded", 243 | "metrics": { 244 | "normal": 4063.334 245 | } 246 | } 247 | ], 248 | "renderer": "focusedChild" 249 | }, 250 | { 251 | "name": "yuppifies", 252 | "metadata": { 253 | "streaming": 1 254 | }, 255 | "nodes": [ 256 | { 257 | "name": "lanterning", 258 | "metrics": { 259 | "danger": 0.18000000000000002, 260 | "normal": 4572.996 261 | } 262 | } 263 | ], 264 | "renderer": "focusedChild" 265 | }, 266 | { 267 | "name": "commanderies", 268 | "metadata": { 269 | "streaming": 1 270 | }, 271 | "nodes": [ 272 | { 273 | "name": "fructiferous", 274 | "metrics": { 275 | "normal": 1918.0560000000003 276 | } 277 | } 278 | ], 279 | "renderer": "focusedChild" 280 | }, 281 | { 282 | "name": "malvasias", 283 | "metadata": { 284 | "streaming": 1 285 | }, 286 | "nodes": [ 287 | { 288 | "name": "langered", 289 | "metrics": { 290 | "danger": 0.03, 291 | "normal": 1853.6560000000002 292 | } 293 | } 294 | ], 295 | "renderer": "focusedChild" 296 | }, 297 | { 298 | "name": "perfectibilians", 299 | "metadata": { 300 | "streaming": 1 301 | }, 302 | "nodes": [ 303 | { 304 | "name": "undercapitalize", 305 | "metrics": { 306 | "normal": 1786.8860000000002 307 | } 308 | } 309 | ], 310 | "renderer": "focusedChild" 311 | }, 312 | { 313 | "name": "nanuas", 314 | "metadata": { 315 | "streaming": 1 316 | }, 317 | "nodes": [ 318 | { 319 | "name": "launches", 320 | "metrics": { 321 | "normal": 1809.534 322 | } 323 | } 324 | ], 325 | "renderer": "focusedChild" 326 | }, 327 | { 328 | "name": "overburning", 329 | "metadata": { 330 | "streaming": 1 331 | }, 332 | "nodes": [ 333 | { 334 | "name": "epistolised", 335 | "metrics": { 336 | "danger": 1.234, 337 | "normal": 3651.6760000000004 338 | } 339 | } 340 | ], 341 | "renderer": "focusedChild" 342 | }, 343 | { 344 | "name": "veniremen", 345 | "metadata": { 346 | "streaming": 1 347 | }, 348 | "nodes": [ 349 | { 350 | "name": "salix", 351 | "metrics": { 352 | "normal": 38.160000000000004 353 | } 354 | }, 355 | { 356 | "name": "ossifrage", 357 | "metrics": { 358 | "normal": 38.142 359 | } 360 | }, 361 | { 362 | "name": "incandesce", 363 | "metrics": { 364 | "normal": 1526.7520000000002 365 | } 366 | } 367 | ], 368 | "renderer": "focusedChild" 369 | }, 370 | { 371 | "name": "quittors", 372 | "metadata": { 373 | "streaming": 1 374 | }, 375 | "nodes": [ 376 | { 377 | "name": "modernised", 378 | "metrics": { 379 | "danger": 12.940000000000001, 380 | "normal": 2329.168 381 | } 382 | } 383 | ], 384 | "renderer": "focusedChild" 385 | }, 386 | { 387 | "name": "gainfully", 388 | "metadata": { 389 | "streaming": 1 390 | }, 391 | "nodes": [ 392 | { 393 | "name": "thewiest", 394 | "metrics": { 395 | "normal": 1159.382 396 | } 397 | } 398 | ], 399 | "renderer": "focusedChild" 400 | }, 401 | { 402 | "name": "unapproachabilities", 403 | "metadata": { 404 | "streaming": 1 405 | }, 406 | "nodes": [ 407 | { 408 | "name": "shikari", 409 | "metrics": { 410 | "normal": 1110.976 411 | } 412 | } 413 | ], 414 | "renderer": "focusedChild" 415 | }, 416 | { 417 | "name": "arabesk", 418 | "metadata": { 419 | "streaming": 1 420 | }, 421 | "nodes": [ 422 | { 423 | "name": "villous", 424 | "metrics": { 425 | "danger": 0.03, 426 | "normal": 976.5799999999999 427 | } 428 | }, 429 | { 430 | "name": "silvan", 431 | "metrics": { 432 | "normal": 24.682000000000002 433 | } 434 | }, 435 | { 436 | "name": "yechs", 437 | "metrics": { 438 | "normal": 24.974000000000004 439 | } 440 | } 441 | ], 442 | "renderer": "focusedChild" 443 | }, 444 | { 445 | "name": "mispricing", 446 | "metadata": { 447 | "streaming": 1 448 | }, 449 | "nodes": [ 450 | { 451 | "name": "pleas", 452 | "metrics": { 453 | "danger": 0.022000000000000002, 454 | "normal": 828.868 455 | } 456 | } 457 | ], 458 | "renderer": "focusedChild" 459 | }, 460 | { 461 | "name": "majordomo", 462 | "metadata": { 463 | "streaming": 1 464 | }, 465 | "nodes": [ 466 | { 467 | "name": "astones", 468 | "metrics": { 469 | "normal": 6.038 470 | } 471 | }, 472 | { 473 | "name": "blamelessnesses", 474 | "metrics": { 475 | "danger": 0.004, 476 | "normal": 1092.528 477 | } 478 | }, 479 | { 480 | "name": "grouched", 481 | "metrics": { 482 | "normal": 6.038 483 | } 484 | } 485 | ], 486 | "renderer": "focusedChild" 487 | }, 488 | { 489 | "name": "tanrec", 490 | "metadata": { 491 | "streaming": 1 492 | }, 493 | "nodes": [ 494 | { 495 | "name": "dogships", 496 | "metrics": { 497 | "normal": 926.2660000000001 498 | } 499 | } 500 | ], 501 | "renderer": "focusedChild" 502 | }, 503 | { 504 | "name": "semitropics", 505 | "metadata": { 506 | "streaming": 1 507 | }, 508 | "nodes": [ 509 | { 510 | "name": "metrifier", 511 | "metrics": { 512 | "normal": 692.9520000000001, 513 | "danger": 0.06999999999999999 514 | } 515 | } 516 | ], 517 | "renderer": "focusedChild" 518 | }, 519 | { 520 | "name": "legwork", 521 | "metadata": { 522 | "streaming": 1 523 | }, 524 | "nodes": [], 525 | "renderer": "focusedChild" 526 | }, 527 | { 528 | "name": "relocator", 529 | "metadata": { 530 | "streaming": 1 531 | }, 532 | "nodes": [ 533 | { 534 | "name": "coho", 535 | "metrics": { 536 | "danger": 0.008, 537 | "normal": 635.724 538 | } 539 | } 540 | ], 541 | "renderer": "focusedChild" 542 | }, 543 | { 544 | "name": "spuds", 545 | "metadata": { 546 | "streaming": 1 547 | }, 548 | "nodes": [ 549 | { 550 | "name": "flamethrowers", 551 | "metrics": { 552 | "normal": 540.194 553 | } 554 | } 555 | ], 556 | "renderer": "focusedChild" 557 | }, 558 | { 559 | "name": "schemozzling", 560 | "metadata": { 561 | "streaming": 1 562 | }, 563 | "nodes": [ 564 | { 565 | "name": "garnishes", 566 | "metrics": { 567 | "danger": 0.05, 568 | "normal": 1264.71 569 | } 570 | } 571 | ], 572 | "renderer": "focusedChild" 573 | }, 574 | { 575 | "name": "profulgent", 576 | "metadata": { 577 | "streaming": 1 578 | }, 579 | "nodes": [ 580 | { 581 | "name": "cockieleekies", 582 | "metrics": { 583 | "normal": 1078.5700000000002 584 | } 585 | } 586 | ], 587 | "renderer": "focusedChild" 588 | }, 589 | { 590 | "name": "brazed", 591 | "metadata": { 592 | "streaming": 1 593 | }, 594 | "nodes": [ 595 | { 596 | "name": "invulnerableness", 597 | "metrics": { 598 | "normal": 429.798 599 | } 600 | } 601 | ], 602 | "renderer": "focusedChild" 603 | }, 604 | { 605 | "name": "gerents", 606 | "metadata": { 607 | "streaming": 1 608 | }, 609 | "nodes": [ 610 | { 611 | "name": "rehabbed", 612 | "metrics": { 613 | "normal": 204.92 614 | } 615 | } 616 | ], 617 | "renderer": "focusedChild" 618 | }, 619 | { 620 | "name": "infectivities", 621 | "metadata": { 622 | "streaming": 1 623 | }, 624 | "nodes": [ 625 | { 626 | "name": "slimy", 627 | "metrics": { 628 | "danger": 39.67400000000001, 629 | "normal": 443.384 630 | } 631 | } 632 | ], 633 | "renderer": "focusedChild" 634 | }, 635 | { 636 | "name": "shidduchim", 637 | "metadata": { 638 | "streaming": 1 639 | }, 640 | "nodes": [ 641 | { 642 | "name": "powhiris", 643 | "metrics": { 644 | "danger": 0.004, 645 | "normal": 527.11 646 | } 647 | } 648 | ], 649 | "renderer": "focusedChild" 650 | }, 651 | { 652 | "name": "prickliest", 653 | "metadata": { 654 | "streaming": 1 655 | }, 656 | "nodes": [ 657 | { 658 | "name": "lampadary", 659 | "metrics": { 660 | "normal": 287.28200000000004 661 | } 662 | } 663 | ], 664 | "renderer": "focusedChild" 665 | }, 666 | { 667 | "name": "imbalmers", 668 | "metadata": { 669 | "streaming": 1 670 | }, 671 | "nodes": [ 672 | { 673 | "name": "imposthumated", 674 | "metrics": { 675 | "danger": 0.004, 676 | "normal": 292.37800000000004 677 | } 678 | } 679 | ], 680 | "renderer": "focusedChild" 681 | }, 682 | { 683 | "name": "blastodiscs", 684 | "metadata": { 685 | "streaming": 1 686 | }, 687 | "nodes": [ 688 | { 689 | "name": "tawiest", 690 | "metrics": { 691 | "normal": 286.03200000000004 692 | } 693 | } 694 | ], 695 | "renderer": "focusedChild" 696 | }, 697 | { 698 | "name": "uraei", 699 | "metadata": { 700 | "streaming": 1 701 | }, 702 | "nodes": [ 703 | { 704 | "name": "pashed", 705 | "metrics": { 706 | "normal": 283.108 707 | } 708 | } 709 | ], 710 | "renderer": "focusedChild" 711 | }, 712 | { 713 | "name": "multiracialisms", 714 | "metadata": { 715 | "streaming": 1 716 | }, 717 | "nodes": [ 718 | { 719 | "name": "cauterises", 720 | "metrics": { 721 | "danger": 1.9000000000000001, 722 | "normal": 289.13000000000005 723 | } 724 | } 725 | ], 726 | "renderer": "focusedChild" 727 | }, 728 | { 729 | "name": "cylindricalness", 730 | "metadata": { 731 | "streaming": 1 732 | }, 733 | "nodes": [ 734 | { 735 | "name": "steen", 736 | "metrics": { 737 | "danger": 6.188000000000001, 738 | "normal": 182.4 739 | } 740 | } 741 | ], 742 | "renderer": "focusedChild" 743 | }, 744 | { 745 | "name": "compellation", 746 | "metadata": { 747 | "streaming": 1 748 | }, 749 | "nodes": [ 750 | { 751 | "name": "malleolar", 752 | "metrics": { 753 | "normal": 34.352, 754 | "danger": 153.10999999999999 755 | } 756 | } 757 | ], 758 | "renderer": "focusedChild" 759 | }, 760 | { 761 | "name": "microparasites", 762 | "metadata": { 763 | "streaming": 1 764 | }, 765 | "nodes": [ 766 | { 767 | "name": "ribbonwoods", 768 | "metrics": { 769 | "danger": 1.5460000000000003, 770 | "normal": 848.7139999999999 771 | } 772 | } 773 | ], 774 | "renderer": "focusedChild" 775 | }, 776 | { 777 | "name": "immedicably", 778 | "metadata": { 779 | "streaming": 1 780 | }, 781 | "nodes": [ 782 | { 783 | "name": "remit", 784 | "metrics": { 785 | "normal": 185.476 786 | } 787 | } 788 | ], 789 | "renderer": "focusedChild" 790 | }, 791 | { 792 | "name": "commerce", 793 | "metadata": { 794 | "streaming": 1 795 | }, 796 | "nodes": [ 797 | { 798 | "name": "gimmes", 799 | "metrics": { 800 | "normal": 180.044 801 | } 802 | } 803 | ], 804 | "renderer": "focusedChild" 805 | }, 806 | { 807 | "name": "alignment", 808 | "metadata": { 809 | "streaming": 1 810 | }, 811 | "nodes": [ 812 | { 813 | "name": "madronos", 814 | "metrics": { 815 | "normal": 156.94000000000003, 816 | "danger": 0.006 817 | } 818 | } 819 | ], 820 | "renderer": "focusedChild" 821 | }, 822 | { 823 | "name": "methadone", 824 | "metadata": { 825 | "streaming": 1 826 | }, 827 | "nodes": [ 828 | { 829 | "name": "fraicheurs", 830 | "metrics": { 831 | "danger": 0.020000000000000004, 832 | "normal": 130.428 833 | } 834 | } 835 | ], 836 | "renderer": "focusedChild" 837 | }, 838 | { 839 | "name": "remarkableness", 840 | "metadata": { 841 | "streaming": 1 842 | }, 843 | "nodes": [ 844 | { 845 | "name": "hydrocortisones", 846 | "metrics": { 847 | "danger": 0.21600000000000003, 848 | "normal": 110.20400000000001 849 | } 850 | } 851 | ], 852 | "renderer": "focusedChild" 853 | }, 854 | { 855 | "name": "reoxidize", 856 | "metadata": { 857 | "streaming": 1 858 | }, 859 | "nodes": [ 860 | { 861 | "name": "trapan", 862 | "metrics": { 863 | "normal": 99.77600000000001, 864 | "danger": 4.946000000000001 865 | } 866 | } 867 | ], 868 | "renderer": "focusedChild" 869 | }, 870 | { 871 | "name": "salvability", 872 | "metadata": { 873 | "streaming": 1 874 | }, 875 | "nodes": [ 876 | { 877 | "name": "belons", 878 | "metrics": { 879 | "danger": 0.148, 880 | "normal": 97.73200000000001 881 | } 882 | } 883 | ], 884 | "renderer": "focusedChild" 885 | }, 886 | { 887 | "name": "playlisted", 888 | "metadata": { 889 | "streaming": 1 890 | }, 891 | "nodes": [], 892 | "renderer": "focusedChild" 893 | }, 894 | { 895 | "name": "karroos", 896 | "metadata": { 897 | "streaming": 1 898 | }, 899 | "nodes": [ 900 | { 901 | "name": "kitschified", 902 | "metrics": { 903 | "danger": 0.8340000000000001, 904 | "normal": 80.06 905 | } 906 | } 907 | ], 908 | "renderer": "focusedChild" 909 | }, 910 | { 911 | "name": "accounts", 912 | "metadata": { 913 | "streaming": 1 914 | }, 915 | "nodes": [ 916 | { 917 | "name": "disfranchise", 918 | "metrics": { 919 | "normal": 72.478, 920 | "danger": 20.21 921 | } 922 | } 923 | ], 924 | "renderer": "focusedChild" 925 | }, 926 | { 927 | "name": "hounding", 928 | "metadata": { 929 | "streaming": 1 930 | }, 931 | "nodes": [], 932 | "renderer": "focusedChild" 933 | }, 934 | { 935 | "name": "priviest", 936 | "metadata": { 937 | "streaming": 1 938 | }, 939 | "nodes": [ 940 | { 941 | "name": "espiers", 942 | "metrics": { 943 | "danger": 0.654, 944 | "normal": 62.326 945 | } 946 | } 947 | ], 948 | "renderer": "focusedChild" 949 | }, 950 | { 951 | "name": "lycee", 952 | "metadata": { 953 | "streaming": 1 954 | }, 955 | "nodes": [ 956 | { 957 | "name": "cybersquattings", 958 | "metrics": { 959 | "normal": 62.026 960 | } 961 | } 962 | ], 963 | "renderer": "focusedChild" 964 | }, 965 | { 966 | "name": "concatenates", 967 | "metadata": { 968 | "streaming": 1 969 | }, 970 | "nodes": [ 971 | { 972 | "name": "versionings", 973 | "metrics": { 974 | "normal": 60.43600000000001 975 | } 976 | } 977 | ], 978 | "renderer": "focusedChild" 979 | }, 980 | { 981 | "name": "oiks", 982 | "metadata": { 983 | "streaming": 1 984 | }, 985 | "nodes": [ 986 | { 987 | "name": "unacquainted", 988 | "metrics": { 989 | "danger": 0.014000000000000002, 990 | "normal": 972.9639999999999 991 | } 992 | } 993 | ], 994 | "renderer": "focusedChild" 995 | }, 996 | { 997 | "name": "nicompoops", 998 | "metadata": { 999 | "streaming": 1 1000 | }, 1001 | "nodes": [ 1002 | { 1003 | "name": "metabolised", 1004 | "metrics": { 1005 | "danger": 2.068, 1006 | "normal": 41.752 1007 | } 1008 | } 1009 | ], 1010 | "renderer": "focusedChild" 1011 | }, 1012 | { 1013 | "name": "corfhouses", 1014 | "metadata": { 1015 | "streaming": 1 1016 | }, 1017 | "nodes": [ 1018 | { 1019 | "name": "anthoid", 1020 | "metrics": { 1021 | "normal": 0.22200000000000003 1022 | } 1023 | } 1024 | ], 1025 | "renderer": "focusedChild" 1026 | }, 1027 | { 1028 | "name": "overdosed", 1029 | "metadata": { 1030 | "streaming": 1 1031 | }, 1032 | "nodes": [ 1033 | { 1034 | "name": "localizable", 1035 | "metrics": { 1036 | "normal": 0.638 1037 | } 1038 | } 1039 | ], 1040 | "renderer": "focusedChild" 1041 | }, 1042 | { 1043 | "name": "rummlegumptions", 1044 | "metadata": { 1045 | "streaming": 1 1046 | }, 1047 | "nodes": [ 1048 | { 1049 | "name": "misprinting", 1050 | "metrics": { 1051 | "danger": 0.014000000000000002, 1052 | "normal": 121.306 1053 | } 1054 | }, 1055 | { 1056 | "name": "revoiced", 1057 | "metrics": { 1058 | "normal": 4.332 1059 | } 1060 | } 1061 | ], 1062 | "renderer": "focusedChild" 1063 | }, 1064 | { 1065 | "name": "precited", 1066 | "metadata": { 1067 | "streaming": 1 1068 | }, 1069 | "nodes": [ 1070 | { 1071 | "name": "radiations", 1072 | "metrics": {} 1073 | } 1074 | ], 1075 | "renderer": "focusedChild" 1076 | }, 1077 | { 1078 | "name": "previsionary", 1079 | "metadata": { 1080 | "streaming": 1 1081 | }, 1082 | "nodes": [ 1083 | { 1084 | "name": "sklenting", 1085 | "metrics": { 1086 | "danger": 0.6240000000000001, 1087 | "normal": 22.904 1088 | } 1089 | } 1090 | ], 1091 | "renderer": "focusedChild" 1092 | }, 1093 | { 1094 | "name": "kaoliangs", 1095 | "metadata": { 1096 | "streaming": 1 1097 | }, 1098 | "nodes": [ 1099 | { 1100 | "name": "allographs", 1101 | "metrics": { 1102 | "normal": 22.882 1103 | } 1104 | } 1105 | ], 1106 | "renderer": "focusedChild" 1107 | }, 1108 | { 1109 | "name": "bypath", 1110 | "metadata": { 1111 | "streaming": 1 1112 | }, 1113 | "nodes": [], 1114 | "renderer": "focusedChild" 1115 | }, 1116 | { 1117 | "name": "hydroxyureas", 1118 | "metadata": { 1119 | "streaming": 1 1120 | }, 1121 | "nodes": [ 1122 | { 1123 | "name": "turms", 1124 | "metrics": { 1125 | "normal": 18.126 1126 | } 1127 | } 1128 | ], 1129 | "renderer": "focusedChild" 1130 | }, 1131 | { 1132 | "name": "gradine", 1133 | "metadata": { 1134 | "streaming": 1 1135 | }, 1136 | "nodes": [ 1137 | { 1138 | "name": "smirches", 1139 | "metrics": { 1140 | "normal": 12.21 1141 | } 1142 | } 1143 | ], 1144 | "renderer": "focusedChild" 1145 | }, 1146 | { 1147 | "name": "atma", 1148 | "metadata": { 1149 | "streaming": 1 1150 | }, 1151 | "nodes": [ 1152 | { 1153 | "name": "palpebrae", 1154 | "metrics": { 1155 | "normal": 433.92600000000004 1156 | } 1157 | } 1158 | ], 1159 | "renderer": "focusedChild" 1160 | }, 1161 | { 1162 | "name": "cleavage", 1163 | "metadata": { 1164 | "streaming": 1 1165 | }, 1166 | "nodes": [ 1167 | { 1168 | "name": "rezzes" 1169 | }, 1170 | { 1171 | "name": "montre", 1172 | "metrics": { 1173 | "normal": 105.25999999999999 1174 | } 1175 | }, 1176 | { 1177 | "name": "alibis", 1178 | "metrics": { 1179 | "danger": 0.004, 1180 | "normal": 105.17200000000001 1181 | } 1182 | } 1183 | ], 1184 | "renderer": "focusedChild" 1185 | }, 1186 | { 1187 | "name": "appropriable", 1188 | "metadata": { 1189 | "streaming": 1 1190 | }, 1191 | "nodes": [ 1192 | { 1193 | "name": "talkathon", 1194 | "metrics": { 1195 | "normal": 613.17, 1196 | "danger": 0.006 1197 | } 1198 | } 1199 | ], 1200 | "renderer": "focusedChild" 1201 | }, 1202 | { 1203 | "metadata": { 1204 | "streaming": 1 1205 | }, 1206 | "nodes": [], 1207 | "name": "INTERNET", 1208 | "renderer": "focusedChild" 1209 | } 1210 | ], 1211 | "connections": [ 1212 | { 1213 | "source": "INTERNET", 1214 | "target": "proxy-prod", 1215 | "metadata": { 1216 | "streaming": 1 1217 | }, 1218 | "metrics": { 1219 | "danger": 97.98200000000001, 1220 | "normal": 32456.61 1221 | } 1222 | }, 1223 | { 1224 | "source": "INTERNET", 1225 | "target": "buglers", 1226 | "metadata": { 1227 | "streaming": 1 1228 | }, 1229 | "metrics": { 1230 | "danger": 18.094, 1231 | "normal": 11850.946000000002 1232 | } 1233 | }, 1234 | { 1235 | "source": "INTERNET", 1236 | "target": "proxy-log", 1237 | "metadata": { 1238 | "streaming": 1 1239 | }, 1240 | "metrics": { 1241 | "warning": 0.006, 1242 | "danger": 1.218, 1243 | "normal": 8232.9 1244 | } 1245 | }, 1246 | { 1247 | "source": "quittors", 1248 | "target": "disseat", 1249 | "metadata": { 1250 | "streaming": 1 1251 | }, 1252 | "metrics": { 1253 | "danger": 0.022000000000000002, 1254 | "normal": 117.234 1255 | } 1256 | }, 1257 | { 1258 | "source": "accounts", 1259 | "target": "salvability", 1260 | "metadata": { 1261 | "streaming": 1 1262 | }, 1263 | "metrics": { 1264 | "danger": 0.288, 1265 | "normal": 71.542 1266 | } 1267 | }, 1268 | { 1269 | "source": "accounts", 1270 | "target": "infectivities", 1271 | "metadata": { 1272 | "streaming": 1 1273 | }, 1274 | "metrics": { 1275 | "danger": 0.002, 1276 | "normal": 74.566 1277 | } 1278 | }, 1279 | { 1280 | "source": "reoxidize", 1281 | "target": "nicompoops", 1282 | "metadata": { 1283 | "streaming": 1 1284 | }, 1285 | "metrics": { 1286 | "normal": 41.47 1287 | } 1288 | }, 1289 | { 1290 | "source": "reoxidize", 1291 | "target": "playlisted", 1292 | "metadata": { 1293 | "streaming": 1 1294 | }, 1295 | "metrics": { 1296 | "normal": 94.49000000000001 1297 | } 1298 | }, 1299 | { 1300 | "source": "reoxidize", 1301 | "target": "precited", 1302 | "metadata": { 1303 | "streaming": 1 1304 | }, 1305 | "metrics": { 1306 | "normal": 37.038000000000004 1307 | } 1308 | }, 1309 | { 1310 | "source": "unapproachabilities", 1311 | "target": "disseat", 1312 | "metadata": { 1313 | "streaming": 1 1314 | }, 1315 | "metrics": { 1316 | "danger": 0.028000000000000004, 1317 | "normal": 133.47 1318 | } 1319 | }, 1320 | { 1321 | "source": "unapproachabilities", 1322 | "target": "cylindricalness", 1323 | "metadata": { 1324 | "streaming": 1 1325 | }, 1326 | "metrics": { 1327 | "danger": 1.1340000000000001, 1328 | "normal": 183.386 1329 | } 1330 | }, 1331 | { 1332 | "source": "unapproachabilities", 1333 | "target": "infectivities", 1334 | "metadata": { 1335 | "streaming": 1 1336 | }, 1337 | "metrics": { 1338 | "danger": 0.016, 1339 | "normal": 10.14 1340 | } 1341 | }, 1342 | { 1343 | "source": "lassoing", 1344 | "target": "spuds", 1345 | "metadata": { 1346 | "streaming": 1 1347 | }, 1348 | "metrics": { 1349 | "danger": 0.004, 1350 | "normal": 27.386000000000003 1351 | } 1352 | }, 1353 | { 1354 | "source": "lassoing", 1355 | "target": "immedicably", 1356 | "metadata": { 1357 | "streaming": 1 1358 | }, 1359 | "metrics": { 1360 | "danger": 0.016, 1361 | "normal": 11.846 1362 | } 1363 | }, 1364 | { 1365 | "source": "lassoing", 1366 | "target": "foetal", 1367 | "metadata": { 1368 | "streaming": 1 1369 | }, 1370 | "metrics": { 1371 | "danger": 43.642, 1372 | "normal": 7428.756 1373 | } 1374 | }, 1375 | { 1376 | "source": "lassoing", 1377 | "target": "neuritics", 1378 | "metadata": { 1379 | "streaming": 1 1380 | }, 1381 | "metrics": { 1382 | "danger": 29.874000000000002, 1383 | "normal": 1947.2400000000002 1384 | } 1385 | }, 1386 | { 1387 | "source": "macrurous", 1388 | "target": "disseat", 1389 | "metadata": { 1390 | "streaming": 1 1391 | }, 1392 | "metrics": { 1393 | "danger": 0.006, 1394 | "normal": 13.984000000000002 1395 | } 1396 | }, 1397 | { 1398 | "source": "macrurous", 1399 | "target": "tanrec", 1400 | "metadata": { 1401 | "streaming": 1 1402 | }, 1403 | "metrics": { 1404 | "danger": 0.002, 1405 | "normal": 134.15 1406 | } 1407 | }, 1408 | { 1409 | "source": "macrurous", 1410 | "target": "semitropics", 1411 | "metadata": { 1412 | "streaming": 1 1413 | }, 1414 | "metrics": { 1415 | "normal": 105.06600000000002, 1416 | "danger": 0.006 1417 | } 1418 | }, 1419 | { 1420 | "source": "macrurous", 1421 | "target": "spuds", 1422 | "metadata": { 1423 | "streaming": 1 1424 | }, 1425 | "metrics": { 1426 | "danger": 0.05600000000000001, 1427 | "normal": 178.37400000000002 1428 | } 1429 | }, 1430 | { 1431 | "source": "macrurous", 1432 | "target": "disenthral", 1433 | "metadata": { 1434 | "streaming": 1 1435 | }, 1436 | "metrics": { 1437 | "normal": 655.186 1438 | } 1439 | }, 1440 | { 1441 | "source": "macrurous", 1442 | "target": "rummlegumptions", 1443 | "metadata": { 1444 | "streaming": 1 1445 | }, 1446 | "metrics": { 1447 | "danger": 0.256, 1448 | "normal": 32.978 1449 | } 1450 | }, 1451 | { 1452 | "source": "macrurous", 1453 | "target": "overdosed", 1454 | "metadata": { 1455 | "streaming": 1 1456 | }, 1457 | "metrics": { 1458 | "danger": 0.256, 1459 | "normal": 32.978 1460 | } 1461 | }, 1462 | { 1463 | "source": "macrurous", 1464 | "target": "corfhouses", 1465 | "metadata": { 1466 | "streaming": 1 1467 | }, 1468 | "metrics": { 1469 | "danger": 0.256, 1470 | "normal": 32.978 1471 | } 1472 | }, 1473 | { 1474 | "source": "macrurous", 1475 | "target": "immedicably", 1476 | "metadata": { 1477 | "streaming": 1 1478 | }, 1479 | "metrics": { 1480 | "danger": 0.118, 1481 | "normal": 109.57600000000001 1482 | } 1483 | }, 1484 | { 1485 | "source": "macrurous", 1486 | "target": "multiracialisms", 1487 | "metadata": { 1488 | "streaming": 1 1489 | }, 1490 | "metrics": { 1491 | "danger": 0.066, 1492 | "normal": 11.408000000000001 1493 | } 1494 | }, 1495 | { 1496 | "source": "macrurous", 1497 | "target": "proletarianised", 1498 | "metadata": { 1499 | "streaming": 1 1500 | }, 1501 | "metrics": { 1502 | "normal": 7.202 1503 | } 1504 | }, 1505 | { 1506 | "source": "macrurous", 1507 | "target": "gainfully", 1508 | "metadata": { 1509 | "streaming": 1 1510 | }, 1511 | "metrics": { 1512 | "danger": 0.012, 1513 | "normal": 1017.616 1514 | } 1515 | }, 1516 | { 1517 | "source": "macrurous", 1518 | "target": "overburning", 1519 | "metadata": { 1520 | "streaming": 1 1521 | }, 1522 | "metrics": { 1523 | "danger": 0.9720000000000001, 1524 | "normal": 160.19000000000003 1525 | } 1526 | }, 1527 | { 1528 | "source": "macrurous", 1529 | "target": "foetal", 1530 | "metadata": { 1531 | "streaming": 1 1532 | }, 1533 | "metrics": { 1534 | "danger": 3.814, 1535 | "normal": 634.038 1536 | } 1537 | }, 1538 | { 1539 | "source": "macrurous", 1540 | "target": "relocator", 1541 | "metadata": { 1542 | "streaming": 1 1543 | }, 1544 | "metrics": { 1545 | "danger": 2.064, 1546 | "normal": 87.34 1547 | } 1548 | }, 1549 | { 1550 | "source": "perfectibilians", 1551 | "target": "disenthral", 1552 | "metadata": { 1553 | "streaming": 1 1554 | }, 1555 | "metrics": { 1556 | "normal": 244.97600000000003 1557 | } 1558 | }, 1559 | { 1560 | "source": "perfectibilians", 1561 | "target": "rummlegumptions", 1562 | "metadata": { 1563 | "streaming": 1 1564 | }, 1565 | "metrics": { 1566 | "danger": 0.054000000000000006, 1567 | "normal": 6.184000000000001 1568 | } 1569 | }, 1570 | { 1571 | "source": "perfectibilians", 1572 | "target": "overdosed", 1573 | "metadata": { 1574 | "streaming": 1 1575 | }, 1576 | "metrics": { 1577 | "danger": 0.054000000000000006, 1578 | "normal": 6.184000000000001 1579 | } 1580 | }, 1581 | { 1582 | "source": "perfectibilians", 1583 | "target": "corfhouses", 1584 | "metadata": { 1585 | "streaming": 1 1586 | }, 1587 | "metrics": { 1588 | "danger": 0.054000000000000006, 1589 | "normal": 6.184000000000001 1590 | } 1591 | }, 1592 | { 1593 | "source": "perfectibilians", 1594 | "target": "overburning", 1595 | "metadata": { 1596 | "streaming": 1 1597 | }, 1598 | "metrics": { 1599 | "danger": 7.18, 1600 | "normal": 1576.49 1601 | } 1602 | }, 1603 | { 1604 | "source": "karroos", 1605 | "target": "tanrec", 1606 | "metadata": { 1607 | "streaming": 1 1608 | }, 1609 | "metrics": { 1610 | "normal": 19.136000000000003 1611 | } 1612 | }, 1613 | { 1614 | "source": "karroos", 1615 | "target": "viraemic", 1616 | "metadata": { 1617 | "streaming": 1 1618 | }, 1619 | "metrics": { 1620 | "danger": 0.022000000000000002, 1621 | "normal": 19.558000000000003 1622 | } 1623 | }, 1624 | { 1625 | "source": "karroos", 1626 | "target": "proletarianised", 1627 | "metadata": { 1628 | "streaming": 1 1629 | }, 1630 | "metrics": { 1631 | "normal": 19.632 1632 | } 1633 | }, 1634 | { 1635 | "source": "cylindricalness", 1636 | "target": "parrocks", 1637 | "metadata": { 1638 | "streaming": 1 1639 | }, 1640 | "metrics": { 1641 | "danger": 0.008, 1642 | "normal": 69.866 1643 | } 1644 | }, 1645 | { 1646 | "source": "arabesk", 1647 | "target": "risks", 1648 | "metadata": { 1649 | "streaming": 1 1650 | }, 1651 | "metrics": { 1652 | "danger": 0.024, 1653 | "normal": 152.236 1654 | } 1655 | }, 1656 | { 1657 | "source": "arabesk", 1658 | "target": "schemozzling", 1659 | "metadata": { 1660 | "streaming": 1 1661 | }, 1662 | "metrics": { 1663 | "normal": 12.118000000000002 1664 | } 1665 | }, 1666 | { 1667 | "source": "arabesk", 1668 | "target": "proletarianised", 1669 | "metadata": { 1670 | "streaming": 1 1671 | }, 1672 | "metrics": { 1673 | "danger": 0.012, 1674 | "normal": 12.094000000000001 1675 | } 1676 | }, 1677 | { 1678 | "source": "arabesk", 1679 | "target": "quittors", 1680 | "metadata": { 1681 | "streaming": 1 1682 | }, 1683 | "metrics": { 1684 | "danger": 3.7460000000000004, 1685 | "normal": 483.74600000000004 1686 | } 1687 | }, 1688 | { 1689 | "source": "atma", 1690 | "target": "hydroxyureas", 1691 | "metadata": { 1692 | "streaming": 1 1693 | }, 1694 | "metrics": { 1695 | "normal": 11.316 1696 | } 1697 | }, 1698 | { 1699 | "source": "atma", 1700 | "target": "bypath", 1701 | "metadata": { 1702 | "streaming": 1 1703 | }, 1704 | "metrics": { 1705 | "danger": 0.08600000000000001, 1706 | "normal": 20.008000000000003 1707 | } 1708 | }, 1709 | { 1710 | "source": "atma", 1711 | "target": "schemozzling", 1712 | "metadata": { 1713 | "streaming": 1 1714 | }, 1715 | "metrics": { 1716 | "danger": 0.004, 1717 | "normal": 441.646 1718 | } 1719 | }, 1720 | { 1721 | "source": "atma", 1722 | "target": "profulgent", 1723 | "metadata": { 1724 | "streaming": 1 1725 | }, 1726 | "metrics": { 1727 | "danger": 0.022000000000000002, 1728 | "normal": 423.578 1729 | } 1730 | }, 1731 | { 1732 | "source": "atma", 1733 | "target": "majordomo", 1734 | "metadata": { 1735 | "streaming": 1 1736 | }, 1737 | "metrics": { 1738 | "danger": 0.978, 1739 | "normal": 422.79600000000005 1740 | } 1741 | }, 1742 | { 1743 | "source": "commerce", 1744 | "target": "disseat", 1745 | "metadata": { 1746 | "streaming": 1 1747 | }, 1748 | "metrics": { 1749 | "danger": 0.008, 1750 | "normal": 212.75400000000002 1751 | } 1752 | }, 1753 | { 1754 | "source": "commerce", 1755 | "target": "shidduchim", 1756 | "metadata": { 1757 | "streaming": 1 1758 | }, 1759 | "metrics": { 1760 | "danger": 0.008, 1761 | "normal": 212.75400000000002 1762 | } 1763 | }, 1764 | { 1765 | "source": "commerce", 1766 | "target": "accounts", 1767 | "metadata": { 1768 | "streaming": 1 1769 | }, 1770 | "metrics": { 1771 | "danger": 0.020000000000000004, 1772 | "normal": 29.962000000000003 1773 | } 1774 | }, 1775 | { 1776 | "source": "commerce", 1777 | "target": "kaoliangs", 1778 | "metadata": { 1779 | "streaming": 1 1780 | }, 1781 | "metrics": { 1782 | "normal": 21.488 1783 | } 1784 | }, 1785 | { 1786 | "source": "commerce", 1787 | "target": "lycee", 1788 | "metadata": { 1789 | "streaming": 1 1790 | }, 1791 | "metrics": { 1792 | "normal": 59.08 1793 | } 1794 | }, 1795 | { 1796 | "source": "commerce", 1797 | "target": "semitropics", 1798 | "metadata": { 1799 | "streaming": 1 1800 | }, 1801 | "metrics": { 1802 | "normal": 16.512, 1803 | "danger": 0.002 1804 | } 1805 | }, 1806 | { 1807 | "source": "commerce", 1808 | "target": "hounding", 1809 | "metadata": { 1810 | "streaming": 1 1811 | }, 1812 | "metrics": { 1813 | "danger": 0.012, 1814 | "normal": 38.32 1815 | } 1816 | }, 1817 | { 1818 | "source": "commerce", 1819 | "target": "spuds", 1820 | "metadata": { 1821 | "streaming": 1 1822 | }, 1823 | "metrics": { 1824 | "danger": 0.03, 1825 | "normal": 26.060000000000002 1826 | } 1827 | }, 1828 | { 1829 | "source": "commerce", 1830 | "target": "priviest", 1831 | "metadata": { 1832 | "streaming": 1 1833 | }, 1834 | "metrics": { 1835 | "normal": 21.996000000000002 1836 | } 1837 | }, 1838 | { 1839 | "source": "commerce", 1840 | "target": "parrocks", 1841 | "metadata": { 1842 | "streaming": 1 1843 | }, 1844 | "metrics": { 1845 | "danger": 0.002, 1846 | "normal": 29.468000000000004 1847 | } 1848 | }, 1849 | { 1850 | "source": "hydroxyureas", 1851 | "target": "risks", 1852 | "metadata": { 1853 | "streaming": 1 1854 | }, 1855 | "metrics": { 1856 | "danger": 0.004, 1857 | "normal": 6.998000000000001 1858 | } 1859 | }, 1860 | { 1861 | "source": "hydroxyureas", 1862 | "target": "schemozzling", 1863 | "metadata": { 1864 | "streaming": 1 1865 | }, 1866 | "metrics": { 1867 | "danger": 0.038000000000000006, 1868 | "normal": 13.176 1869 | } 1870 | }, 1871 | { 1872 | "source": "hydroxyureas", 1873 | "target": "quittors", 1874 | "metadata": { 1875 | "streaming": 1 1876 | }, 1877 | "metrics": { 1878 | "danger": 0.084, 1879 | "normal": 11.338000000000001 1880 | } 1881 | }, 1882 | { 1883 | "source": "hydroxyureas", 1884 | "target": "brazed", 1885 | "metadata": { 1886 | "streaming": 1 1887 | }, 1888 | "metrics": { 1889 | "danger": 0.014000000000000002, 1890 | "normal": 8.328000000000001 1891 | } 1892 | }, 1893 | { 1894 | "source": "overburning", 1895 | "target": "disseat", 1896 | "metadata": { 1897 | "streaming": 1 1898 | }, 1899 | "metrics": { 1900 | "danger": 0.036, 1901 | "normal": 165.91 1902 | } 1903 | }, 1904 | { 1905 | "source": "priviest", 1906 | "target": "shidduchim", 1907 | "metadata": { 1908 | "streaming": 1 1909 | }, 1910 | "metrics": { 1911 | "danger": 0.028000000000000004, 1912 | "normal": 94.644 1913 | } 1914 | }, 1915 | { 1916 | "source": "priviest", 1917 | "target": "accounts", 1918 | "metadata": { 1919 | "streaming": 1 1920 | }, 1921 | "metrics": { 1922 | "danger": 0.012, 1923 | "normal": 42.330000000000005 1924 | } 1925 | }, 1926 | { 1927 | "source": "priviest", 1928 | "target": "salvability", 1929 | "metadata": { 1930 | "streaming": 1 1931 | }, 1932 | "metrics": { 1933 | "danger": 0.010000000000000002, 1934 | "normal": 13.772 1935 | } 1936 | }, 1937 | { 1938 | "source": "priviest", 1939 | "target": "commerce", 1940 | "metadata": { 1941 | "streaming": 1 1942 | }, 1943 | "metrics": { 1944 | "normal": 58.394000000000005 1945 | } 1946 | }, 1947 | { 1948 | "source": "priviest", 1949 | "target": "hounding", 1950 | "metadata": { 1951 | "streaming": 1 1952 | }, 1953 | "metrics": { 1954 | "danger": 0.006, 1955 | "normal": 18.84 1956 | } 1957 | }, 1958 | { 1959 | "source": "salvability", 1960 | "target": "reoxidize", 1961 | "metadata": { 1962 | "streaming": 1 1963 | }, 1964 | "metrics": { 1965 | "danger": 0.10200000000000001, 1966 | "normal": 94.808 1967 | } 1968 | }, 1969 | { 1970 | "source": "methadone", 1971 | "target": "quittors", 1972 | "metadata": { 1973 | "streaming": 1 1974 | }, 1975 | "metrics": { 1976 | "danger": 1.2220000000000002, 1977 | "normal": 130.43800000000002 1978 | } 1979 | }, 1980 | { 1981 | "source": "malvasias", 1982 | "target": "risks", 1983 | "metadata": { 1984 | "streaming": 1 1985 | }, 1986 | "metrics": { 1987 | "danger": 0.072, 1988 | "normal": 584.736 1989 | } 1990 | }, 1991 | { 1992 | "source": "malvasias", 1993 | "target": "oiks", 1994 | "metadata": { 1995 | "streaming": 1 1996 | }, 1997 | "metrics": { 1998 | "danger": 0.010000000000000002, 1999 | "normal": 9.88 2000 | } 2001 | }, 2002 | { 2003 | "source": "malvasias", 2004 | "target": "uraei", 2005 | "metadata": { 2006 | "streaming": 1 2007 | }, 2008 | "metrics": { 2009 | "danger": 0.002, 2010 | "normal": 11.422 2011 | } 2012 | }, 2013 | { 2014 | "source": "alignment", 2015 | "target": "tanrec", 2016 | "metadata": { 2017 | "streaming": 1 2018 | }, 2019 | "metrics": { 2020 | "danger": 0.020000000000000004, 2021 | "normal": 60.664 2022 | } 2023 | }, 2024 | { 2025 | "source": "alignment", 2026 | "target": "viraemic", 2027 | "metadata": { 2028 | "streaming": 1 2029 | }, 2030 | "metrics": { 2031 | "danger": 0.10200000000000001, 2032 | "normal": 143.788 2033 | } 2034 | }, 2035 | { 2036 | "source": "alignment", 2037 | "target": "immedicably", 2038 | "metadata": { 2039 | "streaming": 1 2040 | }, 2041 | "metrics": { 2042 | "danger": 0.052000000000000005, 2043 | "normal": 35.242000000000004 2044 | } 2045 | }, 2046 | { 2047 | "source": "alignment", 2048 | "target": "neuritics", 2049 | "metadata": { 2050 | "streaming": 1 2051 | }, 2052 | "metrics": { 2053 | "danger": 2.152, 2054 | "normal": 143.71 2055 | } 2056 | }, 2057 | { 2058 | "source": "veniremen", 2059 | "target": "disseat", 2060 | "metadata": { 2061 | "streaming": 1 2062 | }, 2063 | "metrics": { 2064 | "normal": 205.9, 2065 | "danger": 0.052000000000000005 2066 | } 2067 | }, 2068 | { 2069 | "source": "neuritics", 2070 | "target": "disenthral", 2071 | "metadata": { 2072 | "streaming": 1 2073 | }, 2074 | "metrics": { 2075 | "danger": 0.006, 2076 | "normal": 952.8299999999999 2077 | } 2078 | }, 2079 | { 2080 | "source": "neuritics", 2081 | "target": "yuppifies", 2082 | "metadata": { 2083 | "streaming": 1 2084 | }, 2085 | "metrics": { 2086 | "danger": 2.9440000000000004, 2087 | "normal": 473.774 2088 | } 2089 | }, 2090 | { 2091 | "source": "infectivities", 2092 | "target": "spuds", 2093 | "metadata": { 2094 | "streaming": 1 2095 | }, 2096 | "metrics": { 2097 | "danger": 0.012, 2098 | "normal": 19.762 2099 | } 2100 | }, 2101 | { 2102 | "source": "commanderies", 2103 | "target": "wickyups", 2104 | "metadata": { 2105 | "streaming": 1 2106 | }, 2107 | "metrics": { 2108 | "danger": 4.296, 2109 | "normal": 3108.2200000000003 2110 | } 2111 | }, 2112 | { 2113 | "source": "commanderies", 2114 | "target": "parrocks", 2115 | "metadata": { 2116 | "streaming": 1 2117 | }, 2118 | "metrics": { 2119 | "danger": 0.034, 2120 | "normal": 13.826 2121 | } 2122 | }, 2123 | { 2124 | "source": "gainfully", 2125 | "target": "mispricing", 2126 | "metadata": { 2127 | "streaming": 1 2128 | }, 2129 | "metrics": { 2130 | "danger": 0.35200000000000004, 2131 | "normal": 813.974 2132 | } 2133 | }, 2134 | { 2135 | "source": "gainfully", 2136 | "target": "parrocks", 2137 | "metadata": { 2138 | "streaming": 1 2139 | }, 2140 | "metrics": { 2141 | "danger": 0.04000000000000001, 2142 | "normal": 51.568 2143 | } 2144 | }, 2145 | { 2146 | "source": "relocator", 2147 | "target": "disenthral", 2148 | "metadata": { 2149 | "streaming": 1 2150 | }, 2151 | "metrics": { 2152 | "normal": 432.53000000000003 2153 | } 2154 | }, 2155 | { 2156 | "source": "relocator", 2157 | "target": "yuppifies", 2158 | "metadata": { 2159 | "streaming": 1 2160 | }, 2161 | "metrics": { 2162 | "danger": 0.782, 2163 | "normal": 107.24400000000001 2164 | } 2165 | }, 2166 | { 2167 | "source": "profulgent", 2168 | "target": "disseat", 2169 | "metadata": { 2170 | "streaming": 1 2171 | }, 2172 | "metrics": { 2173 | "danger": 0.27799999999999997, 2174 | "normal": 1125.488 2175 | } 2176 | }, 2177 | { 2178 | "source": "profulgent", 2179 | "target": "risks", 2180 | "metadata": { 2181 | "streaming": 1 2182 | }, 2183 | "metrics": { 2184 | "danger": 0.17800000000000002, 2185 | "normal": 1024.164 2186 | } 2187 | }, 2188 | { 2189 | "source": "profulgent", 2190 | "target": "uraei", 2191 | "metadata": { 2192 | "streaming": 1 2193 | }, 2194 | "metrics": { 2195 | "danger": 0.032, 2196 | "normal": 242.24400000000003 2197 | } 2198 | }, 2199 | { 2200 | "source": "profulgent", 2201 | "target": "concatenates", 2202 | "metadata": { 2203 | "streaming": 1 2204 | }, 2205 | "metrics": { 2206 | "danger": 0.008, 2207 | "normal": 25.538 2208 | } 2209 | }, 2210 | { 2211 | "source": "profulgent", 2212 | "target": "proletarianised", 2213 | "metadata": { 2214 | "streaming": 1 2215 | }, 2216 | "metrics": { 2217 | "danger": 0.6120000000000001, 2218 | "normal": 107.134 2219 | } 2220 | }, 2221 | { 2222 | "source": "gerents", 2223 | "target": "disseat", 2224 | "metadata": { 2225 | "streaming": 1 2226 | }, 2227 | "metrics": { 2228 | "danger": 0.05600000000000001, 2229 | "normal": 204.30200000000002 2230 | } 2231 | }, 2232 | { 2233 | "source": "gerents", 2234 | "target": "risks", 2235 | "metadata": { 2236 | "streaming": 1 2237 | }, 2238 | "metrics": { 2239 | "danger": 0.026000000000000002, 2240 | "normal": 87.44200000000001 2241 | } 2242 | }, 2243 | { 2244 | "source": "gerents", 2245 | "target": "uraei", 2246 | "metadata": { 2247 | "streaming": 1 2248 | }, 2249 | "metrics": { 2250 | "normal": 10.922 2251 | } 2252 | }, 2253 | { 2254 | "source": "gerents", 2255 | "target": "brazed", 2256 | "metadata": { 2257 | "streaming": 1 2258 | }, 2259 | "metrics": { 2260 | "danger": 0.17400000000000002, 2261 | "normal": 204.236 2262 | } 2263 | }, 2264 | { 2265 | "source": "gradine", 2266 | "target": "infectivities", 2267 | "metadata": { 2268 | "streaming": 1 2269 | }, 2270 | "metrics": { 2271 | "normal": 8.93 2272 | } 2273 | }, 2274 | { 2275 | "source": "viraemic", 2276 | "target": "disseat", 2277 | "metadata": { 2278 | "streaming": 1 2279 | }, 2280 | "metrics": { 2281 | "normal": 732.346, 2282 | "danger": 0.126 2283 | } 2284 | }, 2285 | { 2286 | "source": "viraemic", 2287 | "target": "compellation", 2288 | "metadata": { 2289 | "streaming": 1 2290 | }, 2291 | "metrics": { 2292 | "normal": 180.482 2293 | } 2294 | }, 2295 | { 2296 | "source": "viraemic", 2297 | "target": "risks", 2298 | "metadata": { 2299 | "streaming": 1 2300 | }, 2301 | "metrics": { 2302 | "danger": 0.268, 2303 | "normal": 1447.2420000000002 2304 | } 2305 | }, 2306 | { 2307 | "source": "viraemic", 2308 | "target": "tanrec", 2309 | "metadata": { 2310 | "streaming": 1 2311 | }, 2312 | "metrics": { 2313 | "danger": 0.042, 2314 | "normal": 82.05000000000001 2315 | } 2316 | }, 2317 | { 2318 | "source": "viraemic", 2319 | "target": "commerce", 2320 | "metadata": { 2321 | "streaming": 1 2322 | }, 2323 | "metrics": { 2324 | "danger": 0.004, 2325 | "normal": 110.86400000000002 2326 | } 2327 | }, 2328 | { 2329 | "source": "viraemic", 2330 | "target": "remarkableness", 2331 | "metadata": { 2332 | "streaming": 1 2333 | }, 2334 | "metrics": { 2335 | "danger": 0.254, 2336 | "normal": 104.708 2337 | } 2338 | }, 2339 | { 2340 | "source": "viraemic", 2341 | "target": "nanuas", 2342 | "metadata": { 2343 | "streaming": 1 2344 | }, 2345 | "metrics": { 2346 | "danger": 0.37200000000000005, 2347 | "normal": 1767.798 2348 | } 2349 | }, 2350 | { 2351 | "source": "viraemic", 2352 | "target": "semitropics", 2353 | "metadata": { 2354 | "streaming": 1 2355 | }, 2356 | "metrics": { 2357 | "normal": 533.23, 2358 | "danger": 0.05 2359 | } 2360 | }, 2361 | { 2362 | "source": "viraemic", 2363 | "target": "blastodiscs", 2364 | "metadata": { 2365 | "streaming": 1 2366 | }, 2367 | "metrics": { 2368 | "normal": 281.146 2369 | } 2370 | }, 2371 | { 2372 | "source": "viraemic", 2373 | "target": "prickliest", 2374 | "metadata": { 2375 | "streaming": 1 2376 | }, 2377 | "metrics": { 2378 | "danger": 0.828, 2379 | "normal": 279.064 2380 | } 2381 | }, 2382 | { 2383 | "source": "viraemic", 2384 | "target": "disenthral", 2385 | "metadata": { 2386 | "streaming": 1 2387 | }, 2388 | "metrics": { 2389 | "danger": 0.002, 2390 | "normal": 37.564 2391 | } 2392 | }, 2393 | { 2394 | "source": "viraemic", 2395 | "target": "priviest", 2396 | "metadata": { 2397 | "streaming": 1 2398 | }, 2399 | "metrics": { 2400 | "danger": 0.028000000000000004, 2401 | "normal": 20.408 2402 | } 2403 | }, 2404 | { 2405 | "source": "viraemic", 2406 | "target": "veniremen", 2407 | "metadata": { 2408 | "streaming": 1 2409 | }, 2410 | "metrics": { 2411 | "danger": 0.118, 2412 | "normal": 242.05 2413 | } 2414 | }, 2415 | { 2416 | "source": "viraemic", 2417 | "target": "arabesk", 2418 | "metadata": { 2419 | "streaming": 1 2420 | }, 2421 | "metrics": { 2422 | "danger": 3.1300000000000003, 2423 | "normal": 821.4280000000001 2424 | } 2425 | }, 2426 | { 2427 | "source": "viraemic", 2428 | "target": "methadone", 2429 | "metadata": { 2430 | "streaming": 1 2431 | }, 2432 | "metrics": { 2433 | "danger": 0.072, 2434 | "normal": 128.96400000000003 2435 | } 2436 | }, 2437 | { 2438 | "source": "viraemic", 2439 | "target": "perfectibilians", 2440 | "metadata": { 2441 | "streaming": 1 2442 | }, 2443 | "metrics": { 2444 | "normal": 1814.65, 2445 | "danger": 2.5220000000000002 2446 | } 2447 | }, 2448 | { 2449 | "source": "viraemic", 2450 | "target": "multiracialisms", 2451 | "metadata": { 2452 | "streaming": 1 2453 | }, 2454 | "metrics": { 2455 | "danger": 0.7000000000000001, 2456 | "normal": 174.942 2457 | } 2458 | }, 2459 | { 2460 | "source": "viraemic", 2461 | "target": "proletarianised", 2462 | "metadata": { 2463 | "streaming": 1 2464 | }, 2465 | "metrics": { 2466 | "danger": 1.3820000000000001, 2467 | "normal": 6140.146000000001 2468 | } 2469 | }, 2470 | { 2471 | "source": "viraemic", 2472 | "target": "commanderies", 2473 | "metadata": { 2474 | "streaming": 1 2475 | }, 2476 | "metrics": { 2477 | "danger": 0.41600000000000004, 2478 | "normal": 1858.862 2479 | } 2480 | }, 2481 | { 2482 | "source": "viraemic", 2483 | "target": "gainfully", 2484 | "metadata": { 2485 | "streaming": 1 2486 | }, 2487 | "metrics": { 2488 | "danger": 0.198, 2489 | "normal": 97.298 2490 | } 2491 | }, 2492 | { 2493 | "source": "viraemic", 2494 | "target": "malvasias", 2495 | "metadata": { 2496 | "streaming": 1 2497 | }, 2498 | "metrics": { 2499 | "danger": 1.69, 2500 | "normal": 1827.1240000000003 2501 | } 2502 | }, 2503 | { 2504 | "source": "viraemic", 2505 | "target": "foetal", 2506 | "metadata": { 2507 | "streaming": 1 2508 | }, 2509 | "metrics": { 2510 | "danger": 58.584, 2511 | "normal": 10003.208 2512 | } 2513 | }, 2514 | { 2515 | "source": "viraemic", 2516 | "target": "neuritics", 2517 | "metadata": { 2518 | "streaming": 1 2519 | }, 2520 | "metrics": { 2521 | "danger": 67.046, 2522 | "normal": 4505.166 2523 | } 2524 | }, 2525 | { 2526 | "source": "viraemic", 2527 | "target": "legwork", 2528 | "metadata": { 2529 | "streaming": 1 2530 | }, 2531 | "metrics": { 2532 | "danger": 1.6420000000000003, 2533 | "normal": 647.144 2534 | } 2535 | }, 2536 | { 2537 | "source": "viraemic", 2538 | "target": "relocator", 2539 | "metadata": { 2540 | "streaming": 1 2541 | }, 2542 | "metrics": { 2543 | "danger": 9.032, 2544 | "normal": 502.846 2545 | } 2546 | }, 2547 | { 2548 | "source": "viraemic", 2549 | "target": "brazed", 2550 | "metadata": { 2551 | "streaming": 1 2552 | }, 2553 | "metrics": { 2554 | "danger": 0.382, 2555 | "normal": 187.06400000000002 2556 | } 2557 | }, 2558 | { 2559 | "source": "viraemic", 2560 | "target": "unapproachabilities", 2561 | "metadata": { 2562 | "streaming": 1 2563 | }, 2564 | "metrics": { 2565 | "danger": 2.7420000000000004, 2566 | "normal": 1089.852 2567 | } 2568 | }, 2569 | { 2570 | "source": "viraemic", 2571 | "target": "parrocks", 2572 | "metadata": { 2573 | "streaming": 1 2574 | }, 2575 | "metrics": { 2576 | "danger": 0.544, 2577 | "normal": 1640.478 2578 | } 2579 | }, 2580 | { 2581 | "source": "uraei", 2582 | "target": "disseat", 2583 | "metadata": { 2584 | "streaming": 1 2585 | }, 2586 | "metrics": { 2587 | "normal": 6.04 2588 | } 2589 | }, 2590 | { 2591 | "source": "nanuas", 2592 | "target": "disseat", 2593 | "metadata": { 2594 | "streaming": 1 2595 | }, 2596 | "metrics": { 2597 | "danger": 0.34, 2598 | "normal": 1742.8400000000001 2599 | } 2600 | }, 2601 | { 2602 | "source": "microparasites", 2603 | "target": "viraemic", 2604 | "metadata": { 2605 | "streaming": 1 2606 | }, 2607 | "metrics": { 2608 | "danger": 4.266, 2609 | "normal": 617.72 2610 | } 2611 | }, 2612 | { 2613 | "source": "foetal", 2614 | "target": "disenthral", 2615 | "metadata": { 2616 | "streaming": 1 2617 | }, 2618 | "metrics": { 2619 | "normal": 716.644 2620 | } 2621 | }, 2622 | { 2623 | "source": "foetal", 2624 | "target": "yuppifies", 2625 | "metadata": { 2626 | "streaming": 1 2627 | }, 2628 | "metrics": { 2629 | "danger": 7.944, 2630 | "normal": 1307.4440000000002 2631 | } 2632 | }, 2633 | { 2634 | "source": "imbalmers", 2635 | "target": "risks", 2636 | "metadata": { 2637 | "streaming": 1 2638 | }, 2639 | "metrics": { 2640 | "danger": 0.004, 2641 | "normal": 20.524 2642 | } 2643 | }, 2644 | { 2645 | "source": "imbalmers", 2646 | "target": "veniremen", 2647 | "metadata": { 2648 | "streaming": 1 2649 | }, 2650 | "metrics": { 2651 | "danger": 0.04000000000000001, 2652 | "normal": 65.924 2653 | } 2654 | }, 2655 | { 2656 | "source": "imbalmers", 2657 | "target": "quittors", 2658 | "metadata": { 2659 | "streaming": 1 2660 | }, 2661 | "metrics": { 2662 | "danger": 0.46799999999999997, 2663 | "normal": 65.568 2664 | } 2665 | }, 2666 | { 2667 | "source": "imbalmers", 2668 | "target": "majordomo", 2669 | "metadata": { 2670 | "streaming": 1 2671 | }, 2672 | "metrics": { 2673 | "danger": 0.002, 2674 | "normal": 278.42600000000004 2675 | } 2676 | }, 2677 | { 2678 | "source": "rummlegumptions", 2679 | "target": "spuds", 2680 | "metadata": { 2681 | "streaming": 1 2682 | }, 2683 | "metrics": { 2684 | "danger": 0.18600000000000003, 2685 | "normal": 258.168 2686 | } 2687 | }, 2688 | { 2689 | "source": "semitropics", 2690 | "target": "disseat", 2691 | "metadata": { 2692 | "streaming": 1 2693 | }, 2694 | "metrics": { 2695 | "danger": 1.478, 2696 | "normal": 7.978000000000001 2697 | }, 2698 | "class": "danger" 2699 | }, 2700 | { 2701 | "source": "semitropics", 2702 | "target": "tanrec", 2703 | "metadata": { 2704 | "streaming": 1 2705 | }, 2706 | "metrics": { 2707 | "danger": 0.20800000000000002, 2708 | "normal": 485.086 2709 | } 2710 | }, 2711 | { 2712 | "source": "semitropics", 2713 | "target": "spuds", 2714 | "metadata": { 2715 | "streaming": 1 2716 | }, 2717 | "metrics": { 2718 | "danger": 0.008, 2719 | "normal": 8.76 2720 | } 2721 | }, 2722 | { 2723 | "source": "semitropics", 2724 | "target": "infectivities", 2725 | "metadata": { 2726 | "streaming": 1 2727 | }, 2728 | "metrics": { 2729 | "danger": 0.564, 2730 | "normal": 223.76999999999998 2731 | } 2732 | }, 2733 | { 2734 | "source": "semitropics", 2735 | "target": "previsionary", 2736 | "metadata": { 2737 | "streaming": 1 2738 | }, 2739 | "metrics": { 2740 | "danger": 0.034, 2741 | "normal": 22.124000000000002 2742 | } 2743 | }, 2744 | { 2745 | "source": "schemozzling", 2746 | "target": "risks", 2747 | "metadata": { 2748 | "streaming": 1 2749 | }, 2750 | "metrics": { 2751 | "danger": 0.004, 2752 | "normal": 26.160000000000004 2753 | } 2754 | }, 2755 | { 2756 | "source": "schemozzling", 2757 | "target": "proletarianised", 2758 | "metadata": { 2759 | "streaming": 1 2760 | }, 2761 | "metrics": { 2762 | "danger": 0.286, 2763 | "normal": 6.328 2764 | } 2765 | }, 2766 | { 2767 | "source": "majordomo", 2768 | "target": "risks", 2769 | "metadata": { 2770 | "streaming": 1 2771 | }, 2772 | "metrics": { 2773 | "normal": 414.49399999999997, 2774 | "danger": 0.08000000000000002 2775 | } 2776 | }, 2777 | { 2778 | "source": "majordomo", 2779 | "target": "concatenates", 2780 | "metadata": { 2781 | "streaming": 1 2782 | }, 2783 | "metrics": { 2784 | "normal": 23.726, 2785 | "danger": 0.002 2786 | } 2787 | }, 2788 | { 2789 | "source": "majordomo", 2790 | "target": "veniremen", 2791 | "metadata": { 2792 | "streaming": 1 2793 | }, 2794 | "metrics": { 2795 | "danger": 0.45199999999999996, 2796 | "normal": 971.8940000000001 2797 | } 2798 | }, 2799 | { 2800 | "source": "majordomo", 2801 | "target": "proletarianised", 2802 | "metadata": { 2803 | "streaming": 1 2804 | }, 2805 | "metrics": { 2806 | "normal": 68.152, 2807 | "danger": 0.17600000000000002 2808 | } 2809 | }, 2810 | { 2811 | "source": "majordomo", 2812 | "target": "quittors", 2813 | "metadata": { 2814 | "streaming": 1 2815 | }, 2816 | "metrics": { 2817 | "danger": 4.526, 2818 | "normal": 567.88 2819 | } 2820 | }, 2821 | { 2822 | "source": "cleavage", 2823 | "target": "proletarianised", 2824 | "metadata": { 2825 | "streaming": 1 2826 | }, 2827 | "metrics": { 2828 | "danger": 0.004, 2829 | "normal": 50.62200000000001 2830 | } 2831 | }, 2832 | { 2833 | "source": "cleavage", 2834 | "target": "gerents", 2835 | "metadata": { 2836 | "streaming": 1 2837 | }, 2838 | "metrics": { 2839 | "normal": 51.966 2840 | } 2841 | }, 2842 | { 2843 | "source": "cleavage", 2844 | "target": "majordomo", 2845 | "metadata": { 2846 | "streaming": 1 2847 | }, 2848 | "metrics": { 2849 | "normal": 10.456000000000001 2850 | } 2851 | }, 2852 | { 2853 | "source": "cleavage", 2854 | "target": "parrocks", 2855 | "metadata": { 2856 | "streaming": 1 2857 | }, 2858 | "metrics": { 2859 | "normal": 7.609999999999999 2860 | } 2861 | }, 2862 | { 2863 | "source": "appropriable", 2864 | "target": "oiks", 2865 | "metadata": { 2866 | "streaming": 1 2867 | }, 2868 | "metrics": { 2869 | "normal": 9.846 2870 | } 2871 | }, 2872 | { 2873 | "source": "appropriable", 2874 | "target": "proletarianised", 2875 | "metadata": { 2876 | "streaming": 1 2877 | }, 2878 | "metrics": { 2879 | "danger": 0.006, 2880 | "normal": 139.922 2881 | } 2882 | }, 2883 | { 2884 | "source": "appropriable", 2885 | "target": "gerents", 2886 | "metadata": { 2887 | "streaming": 1 2888 | }, 2889 | "metrics": { 2890 | "normal": 153.304 2891 | } 2892 | }, 2893 | { 2894 | "source": "appropriable", 2895 | "target": "majordomo", 2896 | "metadata": { 2897 | "streaming": 1 2898 | }, 2899 | "metrics": { 2900 | "normal": 30.445999999999998 2901 | } 2902 | }, 2903 | { 2904 | "source": "appropriable", 2905 | "target": "parrocks", 2906 | "metadata": { 2907 | "streaming": 1 2908 | }, 2909 | "metrics": { 2910 | "danger": 0.006, 2911 | "normal": 21.97 2912 | } 2913 | } 2914 | ], 2915 | "maxVolume": 96035.538, 2916 | "props": { 2917 | "maxSemaphores": [ 2918 | { 2919 | "targetRegion": "eu-west-1", 2920 | "region": "us-east-1", 2921 | "value": "20" 2922 | }, 2923 | { 2924 | "targetRegion": "us-west-2", 2925 | "region": "eu-west-1", 2926 | "value": "20" 2927 | }, 2928 | { 2929 | "targetRegion": "us-west-2", 2930 | "region": "us-east-1", 2931 | "value": "200" 2932 | }, 2933 | { 2934 | "targetRegion": "us-east-1", 2935 | "region": "us-west-2", 2936 | "value": "160" 2937 | }, 2938 | { 2939 | "targetRegion": "us-east-1", 2940 | "region": "eu-west-1", 2941 | "value": "20" 2942 | } 2943 | ] 2944 | }, 2945 | "metadata": {}, 2946 | "class": "normal", 2947 | "serverUpdateTime": 1477691777441 2948 | } 2949 | --------------------------------------------------------------------------------