Commit 3e5b44e2 authored by Francis Renaud's avatar Francis Renaud
Browse files

Dashboard -- WIP

parent 2baed6a8
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -12,8 +12,7 @@ import React, { Component } from 'react';
import * as YAML from 'yamljs';
import { Grid, GridCell, GridInner } from '@rmwc/grid';
import { Elevation } from '@rmwc/elevation';
// import IDCVis from '../idc-vis';
import IDCGraph from '../idc-graph';
import IDCVis from '../idc-vis';
import CfgNetworkElementContainer from './cfg-network-element-container';
import CfgPageScenarioButtons from './cfg-page-scenario-buttons';

@@ -534,7 +533,7 @@ class CfgPageContainer extends Component {
                <GridCell span={8}>
                  <Elevation className="component-style" z={2}>
                    <div style={{padding: 10}}>
                      <IDCGraph
                      <IDCVis
                        type={TYPE_CFG}
                        onEditElement={(elem) => this.onEditElement(elem)}
                      />
+95 −163
Original line number Diff line number Diff line
@@ -2,12 +2,15 @@ import _ from 'lodash';
import { connect } from 'react-redux';
import React, { Component }  from 'react';
import { Grid, GridCell, GridInner } from '@rmwc/grid';
import { Elevation } from '@rmwc/elevation';
import { Graph } from 'react-d3-graph';
import ReactDOM from 'react-dom';
import { Button } from '@rmwc/button';
import * as d3 from 'd3';
import axios from 'axios';

import IDCAreaChart from './idc-area-chart';
// import IDCAreaChart from './idc-area-chart';
import IDCLineChart from './idc-line-chart';
import IDCGraph from './idc-graph';
import IDCAppsView from './idc-apps-view';

@@ -18,24 +21,10 @@ import {

import {
  execFakeChangeSelectedDestination,
  execFakeAddPingBucket
  execChangeSourceNodeSelected,
  execAddMetricsEpoch
} from '../state/exec';

const newDataPoint = (date) => {
  const newDate = date || new Date();
  const secs = newDate.getSeconds();
  const newDateString = newDate.toString();
  return {
    'date':newDate,
    'AR':Math.abs(Math.random()*Math.sin(0.05*secs)),
    'DJ':Math.abs(Math.random()*Math.cos(0.05*secs)),
    'MS':Math.abs(Math.random()*Math.sin(0.5*secs)),
    'RC':Math.abs(Math.random()*Math.cos(0.1*secs)),
    'CG':Math.abs(Math.random()*Math.sin(0.2*secs)),
    'RI':Math.abs(Math.random()*Math.sin(3.0*secs))
  };
};

function colorArray(dataLength) {
  const colorScale = d3.interpolateInferno;
  // const colorScale = d3.interpolateMagma;
@@ -59,71 +48,50 @@ function colorArray(dataLength) {
  return colorArray;
}

// const dataStr = '[{"date":"01/08/13","AR":0.1,"DJ":0.35,"MS":0.21,"RC":0.1,"CG":0.1,"RI":0.1},{"date":"01/09/13","AR":0.15,"DJ":0.36,"MS":0.25,"RC":0.15,"CG":0.15,"RI":0.15},{"date":"01/10/13","AR":0.35,"DJ":0.37,"MS":0.27,"RC":0.35,"CG":0.35,"RI":0.35},{"date":"01/11/13","AR":0.38,"DJ":0.22,"MS":0.23,"RC":0.38,"CG":0.38,"RI":0.38},{"date":"01/12/13","AR":0.22,"DJ":0.24,"MS":0.24,"RC":0.22,"CG":0.22,"RI":0.22},{"date":"01/13/13","AR":0.16,"DJ":0.26,"MS":0.21,"RC":0.16,"CG":0.16,"RI":0.16},{"date":"01/14/13","AR":0.07,"DJ":0.34,"MS":0.35,"RC":0.07,"CG":0.07,"RI":0.07},{"date":"01/15/13","AR":0.02,"DJ":0.21,"MS":0.39,"RC":0.02,"CG":0.02,"RI":0.02},{"date":"01/16/13","AR":0.17,"DJ":0.18,"MS":0.4,"RC":0.17,"CG":0.17,"RI":0.17},{"date":"01/17/13","AR":0.33,"DJ":0.45,"MS":0.36,"RC":0.33,"CG":0.33,"RI":0.33},{"date":"01/18/13","AR":0.4,"DJ":0.32,"MS":0.33,"RC":0.4,"CG":0.4,"RI":0.4},{"date":"01/19/13","AR":0.32,"DJ":0.35,"MS":0.43,"RC":0.32,"CG":0.32,"RI":0.32},{"date":"01/20/13","AR":0.26,"DJ":0.3,"MS":0.4,"RC":0.26,"CG":0.26,"RI":0.26},{"date":"01/21/13","AR":0.35,"DJ":0.28,"MS":0.34,"RC":0.35,"CG":0.35,"RI":0.35},{"date":"01/22/13","AR":0.4,"DJ":0.27,"MS":0.28,"RC":0.4,"CG":0.4,"RI":0.4},{"date":"01/23/13","AR":0.32,"DJ":0.26,"MS":0.26,"RC":0.32,"CG":0.32,"RI":0.32},{"date":"01/24/13","AR":0.26,"DJ":0.15,"MS":0.37,"RC":0.26,"CG":0.26,"RI":0.26},{"date":"01/25/13","AR":0.22,"DJ":0.3,"MS":0.41,"RC":0.22,"CG":0.22,"RI":0.22},{"date":"01/26/13","AR":0.16,"DJ":0.35,"MS":0.46,"RC":0.16,"CG":0.16,"RI":0.16},{"date":"01/27/13","AR":0.22,"DJ":0.42,"MS":0.47,"RC":0.22,"CG":0.22,"RI":0.22},{"date":"01/28/13","AR":0.1,"DJ":0.42,"MS":0.41,"RC":0.1,"CG":0.1,"RI":0.1}]';
// const theData = JSON.parse(dataStr);


// const timeParse = d3.timeParse('%m/%d/%y');
// const startingTime = new Date();
// theData.forEach(function(d, i) {
//     const interval = 1000;
//     d.date = new Date(startingTime.getTime() + i*interval);
// });
const metricsBasePath = 'http://10.3.16.73:30008/v1';

// const recentData = end => start => dataPoint => {
//     const dataPointMilli  = dataPoint.date.getTime();
//     return start.getTime() <= dataPointMilli && dataPointMilli <= end.getTime();
// };

const updateData = (data) => {

  let newData;
  if (!data.length) {
    newData = [newDataPoint()];
  } else {
    newData = data.slice(1).concat([newDataPoint()]);
const dataPointFromEpochDataPoints = destinations => sourceNodeId => dataAccessor => epochDataPoints => {
  if (!epochDataPoints.length) {
    return null;
  }

  return newData;
};

const dataPointFromBucket = keys => b => {
  let dp = {
    date: b.date
    date: epochDataPoints[0].timestamp
  };

  const accessor = p => p.delay;
  const avgForKey = pings => acc => key => {
    const pingsForKeyDestination = pings.filter(p => p.dest === key);
    const avg = d3.mean(pingsForKeyDestination, acc);
  const avgForDest = dataPoints => acc => dest => {
    const hasSource = src => p => p.src === src;
    const hasDestination = dest => p => p.dest === dest;
    
    const dataPointsForDestSource = dataPoints
      .filter(hasSource(sourceNodeId))
      .filter(hasDestination(dest));
    const avg = d3.mean(dataPointsForDestSource, acc);
    return avg;
  };
  
  keys.forEach(k => {
    dp[k] = avgForKey(b.pings)(accessor)(k) || 0;
  destinations.forEach(dest => {
    dp[dest] = avgForDest(epochDataPoints)(dataAccessor)(dest) || 0;
  });

  return dp;
};

const pingBucketsToData = pingBuckets => nb => keys => {
  const buckets = pingBuckets.slice(-nb);
  const dataPoints = buckets.map(dataPointFromBucket(keys));
const notNull = x => x;
const epochsToDataPoints = epochs => nb => destinations => dataAccessor => sourceNodeId => {
  const selectedEpochs = epochs.length ? epochs.slice(-nb) : [];
  const dataPoints = selectedEpochs.map(dataPointFromEpochDataPoints(destinations)(sourceNodeId)(dataAccessor)).filter(notNull);
  return dataPoints;
};

const maxValue = pingBuckets => {
  const max = d3.max(pingBuckets, b => {
    return d3.max(b.pings, p => p.delay);
  });
  return max;
};

const minValue = pingBuckets => {
  const min = d3.min(pingBuckets, b => {
    return d3.min(b.pings, p => p.delay);
  });
  return min;
const dataAccessorForType = dataType => {
  switch (dataType) {
  case 'latency':
    return p => p.data.latency;
  case 'ingressPacketStats':
    return p => p.data.throughput;
  default:
    return dataAccessorForType('latency');
  }
};

class DashboardContainer extends Component {
@@ -136,84 +104,25 @@ class DashboardContainer extends Component {
  }

  componentDidMount() {

    // Initial data
    // let theData = [];
    // const initialNbPoints = 25;
    // const now = new Date();
    // for (let i=0; i< initialNbPoints; i++) {
    //   theData.push(newDataPoint(new Date(now.getTime() + (i - initialNbPoints)*1000)));
    // }

    // this.setState({data: theData});
    // const that = this;
    // this.timer = setInterval(() => {
    //   that.setState({
    //     data: updateData(that.state.data)
    //   });
    // }, 1000);

    this.bucketCount = 0;
    this.dataTimer = setInterval(() => this.nextDataBucket(this.bucketCount), 1000);
  }

  componendDidUnmount() {
    clearInterval(this.dataTimer);
  }

  nextDataBucket() {
    this.apps = this.getRoot().descendants().filter(isApp);
    const nbNewPings = this.apps.length*10;
    const srcNodeIndex = () => {
      return Math.floor((Math.random()*this.apps.length));
    };

    const destNodeIndex = (srcIdx) => {
      // const destIdx = (srcIdx + 1 + Math.ceil(Math.random()*2)*2) % apps.length;
      const destIdx = (srcIdx + 1 + Math.ceil((Math.random()*this.apps.length - 1))) % this.apps.length;
      // const index = Math.floor(Math.random()*destinations.length);
      return destIdx;
    };

    const funcs = [x => 0.2*(1 + Math.sin(x-5)), x => 0.2*(1 + Math.cos(x)), x => 0.2 * (1 + Math.random() * Math.cos(x)*Math.sin(x-12))];

    const newPing = (date, i, bucketCount) => {
      const srcIdx = srcNodeIndex();
      const destIdx = destNodeIndex(srcIdx);
      const delay = Math.random() + 0.2;

      const amplitude = 0.2*(destIdx % 3)*(destIdx%5) + 1;
      const frequency = 0.3*(destIdx % 3)*(destIdx%5) + 1;
      const phase = (destIdx % 5)*(destIdx%7);
      const x = bucketCount % 25;
      const func = funcs[destIdx % 3];
      const ping = {
        src: this.apps[srcIdx].data.id,
        dest: this.apps[destIdx].data.id,
        date: date,
        delay: amplitude*func((x - phase)*frequency)
    this.epochCount = 0;
    const nextData = () => {
      this.epochCount += 1;
      this.fetchMetrics();
    };

      return ping;
    };

    let newPings = [];
    const now = new Date();
    for(let i=0; i < nbNewPings; i++) {
      newPings.push(newPing(now, i, this.bucketCount));
    this.dataTimer = setInterval(nextData, 1000);
  }

    const dataBucket = {
      date: now,
      pings: newPings
    };

    this.props.addPingBucket(dataBucket);
    this.bucketCount += 1;
  componentWillUnmount() {
    clearInterval(this.dataTimer);
  }

  componentWillUnmount() {
    clearInterval(this.timer);
  fetchMetrics() {
    return axios.get(`${metricsBasePath}/metrics?startTime=now-6s&stopTime=now`)
      .then(res => {
        this.props.addMetricsEpoch(res.data.dataResponse || []);
      }).catch((e) => {
        console.log('Error while fetching metrics', e);
      });
  }

  getRoot() {
@@ -221,34 +130,48 @@ class DashboardContainer extends Component {
  }

  render() {

    const root = this.getRoot();
    const nodes = root.descendants();
   
    const nbBuckets = this.props.nbBuckets || 25;
    const max = maxValue(this.props.pingBuckets);
    const min = minValue(this.props.pingBuckets);

    const apps = nodes.filter(isApp);
    const destinations = apps.map(a => a.data.id);
    const colorRange = colorArray(destinations.length);
    const dataPoints = pingBucketsToData(this.props.pingBuckets)(nbBuckets)(destinations);
    const nbEpochs = 25;

    const selectedNodeId = this.props.sourceNodeSelected ? this.props.sourceNodeSelected.data.id : null;
    const dataAccessor = dataAccessorForType(this.props.dataTypeSelected);
    const dataPoints = epochsToDataPoints(this.props.epochs)(nbEpochs)(destinations)(dataAccessor)(selectedNodeId);

    const showApps = this.props.showAppsView;
    const span = showApps ? 6 : 12;

    const colorForApp = apps.reduce((res, val, i) => {
      // res[val.data.id] = colorRange[i];
      return {...res, [val.data.id]: colorRange[i]};
    }, {});

    const lastEpoch = this.props.epochs.length ? this.props.epochs.slice(-1)[0] : [];
    const isDataOfType = type => dataPoint => dataPoint.dataType === type;
    const data = lastEpoch.filter(isDataOfType(this.props.dataTypeSelected));

    let graph = null;

    if (showApps) {
      graph = (
        <IDCAppsView
          apps={apps}
          pingBuckets={this.props.pingBuckets}
          colorRange={colorRange}
          width={700}
          height={600}

          onNodeClicked={() => {}}
          data={data}
          dataAccessor={dataAccessor}
          dataType={this.props.dataTypeSelected}
          selectedSource={selectedNodeId}
          colorForApp={colorForApp}
          onNodeClicked={(e) => {
            console.log('Node clicked is: ', e.node);
            this.props.changeSourceNodeSelected(e.node);
          }}
        />
      );
    } else {
@@ -260,20 +183,26 @@ class DashboardContainer extends Component {

    return (
      <Grid>
        <GridCell span={span}>
        <GridCell span={span} style={{marginLeft: -10}}>
          <Elevation z={4}>
            {graph}
          </Elevation>
        </GridCell>

        {showApps ? (<GridCell span={6}>
          <IDCAreaChart
        {showApps ? (<GridCell span={6}  style={{marginRight: -10}}>
          <Elevation z={4}>
            <IDCLineChart
              data={dataPoints}
              width={700} height={600}
              destinations={destinations}
              colorRange={colorRange}
            onKeySelected={(dest) => this.props.changeSelectedDestination(dest)}
            min={min}
            max={max}
              sourceSelected={this.props.sourceNodeSelected}
              // min={min}
              // max={max}
              colorForApp={colorForApp}
            />
          </Elevation>
          
        </GridCell>) 
          : null}
      </Grid>
@@ -284,15 +213,18 @@ class DashboardContainer extends Component {

const mapStateToProps = state => {
  return {
    pingBuckets: state.exec.fakeData.pingBuckets,
    displayedScenario: state.exec.displayedScenario
    displayedScenario: state.exec.displayedScenario,
    epochs: state.exec.metrics.epochs,
    sourceNodeSelected: state.exec.metrics.sourceNodeSelected,
    dataTypeSelected: state.exec.metrics.dataTypeSelected
  };
};

const mapDispatchToProps = dispatch => {
  return {
    changeSelectedDestination: (dest) => dispatch(execFakeChangeSelectedDestination(dest)),
    addPingBucket: (b) => dispatch(execFakeAddPingBucket(b))
    changeSourceNodeSelected: (src) => dispatch(execChangeSourceNodeSelected(src)),
    addMetricsEpoch: (epoch) => dispatch(execAddMetricsEpoch(epoch))
  };
};

+2 −4
Original line number Diff line number Diff line
@@ -266,14 +266,13 @@ class ExecPageContainer extends Component {
        {this.props.exec.state.scenario !== EXEC_STATE_IDLE &&
          <>
              <Grid style={{width: '100%'}}>
                <GridInner>
                  <GridCell span={spanLeft}>
                    <Elevation className="component-style" z={2}>
                    {/* <Elevation className="component-style" z={2}> */}
                      <div style={{padding: 10}}>
                        {this.props.experimental ? (<DashboardContainer showAppsView={true}/>) : (<IDCVis type={TYPE_EXEC} />)}
                        
                      </div>
                    </Elevation>
                    {/* </Elevation> */}
                  </GridCell>
                  <GridCell span={spanRight} hidden={!this.props.eventCreationMode} style={styles.inner}>
                    <Elevation className="component-style" z={2}>
@@ -285,7 +284,6 @@ class ExecPageContainer extends Component {
                      />
                    </Elevation>
                  </GridCell>
                </GridInner>
              </Grid>
          </>
        }
+97 −65
Original line number Diff line number Diff line
@@ -18,10 +18,69 @@ import {
  lineGeneratorNodes
} from './graph-utils';

import {
  getScenarioNodeChildren
} from '../util/scenario-utils';
const edgesFromData = (data, dataAccessor, colorForApp, selectedSource) => {
  const pings = data;
  let m = {};
  _.each(pings, p => {
    if (!m[p.src]) {
      m[p.src] = {};
    }
 
    if (!m[p.src][p.dest]) {
      m[p.src][p.dest] = {
        pings: []
      };
    }
 
    const o = m[p.src][p.dest];
    o.pings.push(p);
  });

  const apps = Object.keys(m);

  console.log('m: ', m);
 
  const edgesFromSource = dataAccessor => src => {
    const rowObject = m[src];
    if (!rowObject) {
      return [];
    }
    const destinations = Object.keys(m[src]);

    const edgesFromDestinations = (dest) => {
      // To debug
      const dataFromPing = p => {
        if (dataAccessor(p)) {
          console.log('Bad value!');  
        }
        return dataAccessor(p);
      };

      if (!d3.mean(rowObject[dest].pings, dataAccessor)) {
        console.log('Bad value!');
      }
      return  {
        src: src,
        dest: dest,
        count: rowObject[dest].pings.length,
        color: colorForApp[dest],
        avgData: d3.mean(rowObject[dest].pings, dataAccessor)
      };
    };
    return _.map(destinations, edgesFromDestinations);
  };

  const outwardEdgesIfSourceSelected = e => {
    if (selectedSource) {
      return e.src === selectedSource;
    } else {
      return false;
    }
  };
  const edges = _.flatMap(apps.map(edgesFromSource(dataAccessor))).filter(outwardEdgesIfSourceSelected);

  return edges; 
};

const positionAppsCircle = ({apps, width, height}) => {
  const cx = width/2.0;
@@ -36,86 +95,59 @@ const positionAppsCircle = ({apps, width, height}) => {
  });
};

const edgeLabelForDataType = type => {
  switch(type) {
  case 'latency':
    return 'Latency: ';
  case 'ingressPacketStats':
    return 'Throughput: ';
  default:
    return '';
  }
};

const unitsForDataType = type => {
  switch(type) {
  case 'latency':
    return 'ms';
  case 'ingressPacketStats':
    return 'Kbps';
  default:
    return '';
  }
};


const IDCAppsView = (
  {
    apps,
    colorRange,
    selectedSource,
    pingBuckets,
    data,
    dataAccessor,
    dataType,
    width,
    height,
    onNodeClicked
    onNodeClicked,
    colorForApp
  }
) => {
  
  const [positioningNeeded, setPositioningNeeded] = useState(true);

  

  const colorForApp = apps.reduce((res, val, i) => {
    // res[val.data.id] = colorRange[i];
    return {...res, [val.data.id]: colorRange[i]};
  }, {});

  //if (positioningNeeded) {
  // copyAttributesRecursive(data)(this.root);
  positionAppsCircle({apps: apps, height: height, width: width});
  //setPositioningNeeded(false);
  //}

  const pingBucket = _.last(pingBuckets);

  if (!pingBucket) {
    return null;
  }

  const appsMap = {};
  _.each(apps, a => appsMap[a.data.id] = a);

  const pings = pingBucket.pings;
  let m = {};
  _.each(pings, p => {
    if (!m[p.src]) {
      m[p.src] = {};
    }
  const edges = edgesFromData(data.filter(dataAccessor), dataAccessor, colorForApp, selectedSource);

    if (!m[p.src][p.dest]) {
      m[p.src][p.dest] = {
        pings: []
      };
    }

    const o = m[p.src][p.dest];
    o.pings.push(p);
  });

  const edges = _.flatMap(apps
    .map((d) => {
      const rowObject = m[d.data.id];
      if (!rowObject) {
        return [];
      }
      const destinations = Object.keys(m[d.data.id]);
      return _.map(destinations, (dest) => {
        return  {
          src: d.data.id,
          dest: dest,
          count: rowObject[dest].pings.length,
          color: colorForApp[dest],
          avgLatency: d3.mean(rowObject[dest].pings, d => d.delay)
        };
      });
    }
    )
  ).filter(e => {
    // return nbSelected ? appsMap[e.src].selected : true;
    // console.log(`${appsMap[e.src].data.id}:`, appsMap[e.src]);
    if (selectedSource) {
      return e.src === selectedSource;
    } else {
      return false;
    }
  });
  const edgeLabel = edgeLabelForDataType(dataType);
  const edgeUnits = unitsForDataType(dataType);

  const lineDefs = 
    <defs>
@@ -148,7 +180,7 @@ const IDCAppsView = (
        xlinkHref={`#textPathDef${i}`}
        startOffset={'45%'}
      >
        {`Avg lat: ${e.avgLatency.toFixed(2)} ms`}
        {`${edgeLabel} ${e.avgData.toFixed(2)} ${edgeUnits}`}
          
      </textPath>
    </text>
+2 −2
Original line number Diff line number Diff line
@@ -52,7 +52,7 @@ const IDCAreaChart = props => {

        const amplitude = colorRange.length * (props.max - props.min);
        // const yRange = [-0.5*amplitude, 0.5*amplitude];
        const yRange = [0, 7];
        const yRange = [0, 400];
        const timeRange = d3.extent(data, d => new Date(d.date));
        const x = d3.scaleTime().domain(timeRange).range([0, width]);
        const y = d3.scaleLinear().domain(yRange).range([height - 50, 0]);
@@ -64,7 +64,7 @@ const IDCAreaChart = props => {
          .tickSize(0.01);
        // const yAxisr = d3.axisLeft(y);

        const keys = props.destinations;
        const keys = props.sources;
        const stack = d3.stack().keys(keys);//.offset(d3.stackOffsetSilhouette);//.order(d3.stackOrderInsideOut);

        const area = d3.area()
Loading