You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by om...@apache.org on 2015/12/08 07:37:43 UTC

[19/51] [partial] incubator-metron git commit: Initial import of code from https://github.com/OpenSOC/opensoc at ac0b00373f8f56dfae03a8109af5feb373ea598e.

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/05e188ba/opensoc-ui/lib/public/app/panels/table/module.js
----------------------------------------------------------------------
diff --git a/opensoc-ui/lib/public/app/panels/table/module.js b/opensoc-ui/lib/public/app/panels/table/module.js
new file mode 100755
index 0000000..c3fef73
--- /dev/null
+++ b/opensoc-ui/lib/public/app/panels/table/module.js
@@ -0,0 +1,516 @@
+/** @scratch /panels/5
+ *
+ * include::panels/table.asciidoc[]
+ */
+
+/** @scratch /panels/table/0
+ *
+ * == table
+ * Status: *Stable*
+ *
+ * The table panel contains a sortable, pagable view of documents that. It can be arranged into
+ * defined columns and offers several interactions, such as performing adhoc terms aggregations.
+ *
+ */
+define([
+  'angular',
+  'app',
+  'lodash',
+  'kbn',
+  'moment',
+],
+function (angular, app, _, kbn, moment) {
+  'use strict';
+
+  var module = angular.module('kibana.panels.table', []);
+  app.useModule(module);
+
+  module.controller('table', function($rootScope, $scope, $modal, $q, $compile, $timeout,
+    fields, querySrv, dashboard, filterSrv) {
+    $scope.panelMeta = {
+      modals : [
+        {
+          description: "Inspect",
+          icon: "icon-info-sign",
+          partial: "app/partials/inspector.html",
+          show: $scope.panel.spyable
+        }
+      ],
+      editorTabs : [
+        {
+          title:'Paging',
+          src: 'app/panels/table/pagination.html'
+        },
+        {
+          title:'Queries',
+          src: 'app/partials/querySelect.html'
+        }
+      ],
+      status: "Stable",
+      description: "A paginated table of records matching your query or queries. Click on a row to "+
+        "expand it and review all of the fields associated with that document. <p>"
+    };
+
+    // Set and populate defaults
+    var _d = {
+      /** @scratch /panels/table/5
+       * === Parameters
+       *
+       * size:: The number of hits to show per page
+       */
+      size    : 100, // Per page
+      /** @scratch /panels/table/5
+       * pages:: The number of pages available
+       */
+      pages   : 5,   // Pages available
+      /** @scratch /panels/table/5
+       * offset:: The current page
+       */
+      offset  : 0,
+      /** @scratch /panels/table/5
+       * sort:: An array describing the sort order of the table. For example [`@timestamp',`desc']
+       */
+      sort    : ['_score','desc'],
+      /** @scratch /panels/table/5
+       * overflow:: The css overflow property. `min-height' (expand) or `auto' (scroll)
+       */
+      overflow: 'min-height',
+      /** @scratch /panels/table/5
+       * fields:: the fields used a columns of the table, in an array.
+       */
+      fields  : [],
+      /** @scratch /panels/table/5
+       * highlight:: The fields on which to highlight, in an array
+       */
+      highlight : [],
+      /** @scratch /panels/table/5
+       * sortable:: Set sortable to false to disable sorting
+       */
+      sortable: true,
+      /** @scratch /panels/table/5
+       * header:: Set to false to hide the table column names
+       */
+      header  : true,
+      /** @scratch /panels/table/5
+       * paging:: Set to false to hide the paging controls of the table
+       */
+      paging  : true,
+      /** @scratch /panels/table/5
+       * field_list:: Set to false to hide the list of fields. The user will be able to expand it,
+       * but it will be hidden by default
+       */
+      field_list: true,
+      /** @scratch /panels/table/5
+       * all_fields:: Set to true to show all fields in the mapping, not just the current fields in
+       * the table.
+       */
+      all_fields: false,
+      /** @scratch /panels/table/5
+       * trimFactor:: The trim factor is the length at which to truncate fields takinging into
+       * consideration the number of columns in the table. For example, a trimFactor of 100, with 5
+       * columns in the table, would trim each column at 20 character. The entirety of the field is
+       * still available in the expanded view of the event.
+       */
+      trimFactor: 300,
+      /** @scratch /panels/table/5
+       * localTime:: Set to true to adjust the timeField to the browser's local time
+       */
+      localTime: false,
+      /** @scratch /panels/table/5
+       * timeField:: If localTime is set to true, this field will be adjusted to the browsers local time
+       */
+      timeField: '@timestamp',
+      /** @scratch /panels/table/5
+       * spyable:: Set to false to disable the inspect icon
+       */
+      spyable : true,
+      /** @scratch /panels/table/5
+       *
+       * ==== Queries
+       * queries object:: This object describes the queries to use on this panel.
+       * queries.mode::: Of the queries available, which to use. Options: +all, pinned, unpinned, selected+
+       * queries.ids::: In +selected+ mode, which query ids are selected.
+       */
+      queries     : {
+        mode        : 'all',
+        ids         : []
+      },
+      /*
+       * locked:: whether to lock the query, preventing it from being affected by filters
+       */
+      locked: false,
+      style   : {'font-size': '9pt'},
+      normTimes : true,
+    };
+    _.defaults($scope.panel,_d);
+
+    $scope.init = function () {
+      $scope.columns = {};
+      _.each($scope.panel.fields,function(field) {
+        $scope.columns[field] = true;
+      });
+
+      $scope.Math = Math;
+      $scope.identity = angular.identity;
+      $scope.$on('refresh',function(){$scope.get_data();});
+
+      $scope.fields = fields;
+      $scope.get_data();
+    };
+
+    // Create a percent function for the view
+    $scope.percent = kbn.to_percent;
+
+    $scope.closeFacet = function() {
+      if($scope.modalField) {
+        delete $scope.modalField;
+      }
+    };
+
+    $scope.termsModal = function(field,chart) {
+      $scope.closeFacet();
+      $timeout(function() {
+        $scope.modalField = field;
+        showModal(
+          '{"height":"200px","chart":"'+chart+'","field":"'+field+'"}','terms');
+      },0);
+    };
+
+    $scope.statsModal = function(field) {
+      $scope.modalField = field;
+      showModal(
+        '{"field":"'+field+'"}','statistics');
+    };
+
+    var showModal = function(panel,type) {
+      $scope.facetPanel = panel;
+      $scope.facetType = type;
+
+      // create a new modal. Can't reuse one modal unforunately as the directive will not
+      // re-render on show.
+      /*
+      $modal({
+        template: './app/panels/table/modal.html',
+        persist: false,
+        show: true,
+        scope: $scope.$new(),
+        keyboard: false
+      });
+      */
+
+    };
+
+
+
+    $scope.toggle_micropanel = function(field,groups) {
+      var docs = _.map($scope.data,function(_d){return _d.kibana._source;});
+      var topFieldValues = kbn.top_field_values(docs,field,10,groups);
+      $scope.micropanel = {
+        field: field,
+        grouped: groups,
+        values : topFieldValues.counts,
+        hasArrays : topFieldValues.hasArrays,
+        related : kbn.get_related_fields(docs,field),
+        limit: 10,
+        count: _.countBy(docs,function(doc){return _.contains(_.keys(doc),field);})['true']
+      };
+    };
+
+    $scope.micropanelColor = function(index) {
+      var _c = ['bar-success','bar-warning','bar-danger','bar-info','bar-primary'];
+      return index > _c.length ? '' : _c[index];
+    };
+
+    $scope.set_sort = function(field) {
+      if($scope.panel.sort[0] === field) {
+        $scope.panel.sort[1] = $scope.panel.sort[1] === 'asc' ? 'desc' : 'asc';
+      } else {
+        $scope.panel.sort[0] = field;
+      }
+      $scope.get_data();
+    };
+
+    $scope.toggle_field = function(field) {
+      if (_.indexOf($scope.panel.fields,field) > -1) {
+        $scope.panel.fields = _.without($scope.panel.fields,field);
+        delete $scope.columns[field];
+      } else {
+        $scope.panel.fields.push(field);
+        $scope.columns[field] = true;
+      }
+    };
+
+    $scope.toggle_highlight = function(field) {
+      if (_.indexOf($scope.panel.highlight,field) > -1) {
+        $scope.panel.highlight = _.without($scope.panel.highlight,field);
+      } else {
+        $scope.panel.highlight.push(field);
+      }
+    };
+
+    $scope.toggle_details = function(row) {
+      row.kibana.details = row.kibana.details ? false : true;
+      row.kibana.view = row.kibana.view || 'table';
+      //row.kibana.details = !row.kibana.details ? $scope.without_kibana(row) : false;
+    };
+
+    $scope.page = function(page) {
+      $scope.panel.offset = page*$scope.panel.size;
+      $scope.get_data();
+    };
+
+    $scope.build_search = function(field,value,negate) {
+      var query;
+      // This needs to be abstracted somewhere
+      if(_.isArray(value)) {
+        query = "(" + _.map(value,function(v){return angular.toJson(v);}).join(" AND ") + ")";
+      } else if (_.isUndefined(value)) {
+        query = '*';
+        negate = !negate;
+      } else {
+        query = angular.toJson(value);
+      }
+      $scope.panel.offset = 0;
+      filterSrv.set({type:'field',field:field,query:query,mandate:(negate ? 'mustNot':'must')});
+    };
+
+    $scope.fieldExists = function(field,mandate) {
+      filterSrv.set({type:'exists',field:field,mandate:mandate});
+    };
+
+    $scope.get_data = function(segment,query_id) {
+      var
+        _segment,
+        request,
+        boolQuery,
+        queries,
+        sort;
+
+      $scope.panel.error =  false;
+
+      // Make sure we have everything for the request to complete
+      if(dashboard.indices.length === 0) {
+        return;
+      }
+
+      sort = [$scope.ejs.Sort($scope.panel.sort[0]).order($scope.panel.sort[1])];
+      if($scope.panel.localTime) {
+        sort.push($scope.ejs.Sort($scope.panel.timeField).order($scope.panel.sort[1]));
+      }
+
+
+      $scope.panelMeta.loading = true;
+
+      _segment = _.isUndefined(segment) ? 0 : segment;
+      $scope.segment = _segment;
+
+      request = $scope.ejs.Request().indices(dashboard.indices[_segment]);
+
+      $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
+
+      queries = querySrv.getQueryObjs($scope.panel.queries.ids);
+      console.log(queries)
+
+      boolQuery = $scope.ejs.BoolQuery();
+      _.each(queries,function(q) {
+        boolQuery = boolQuery.should(querySrv.toEjsObj(q));
+      });
+
+      request = request.query(
+        $scope.ejs.FilteredQuery(
+          boolQuery,
+          $scope.panel.locked ? null : filterSrv.getBoolFilter(filterSrv.ids())
+        ))
+        .highlight(
+          $scope.ejs.Highlight($scope.panel.highlight)
+          .fragmentSize(2147483647) // Max size of a 32bit unsigned int
+          .preTags('@start-highlight@')
+          .postTags('@end-highlight@')
+        )
+        .size($scope.panel.size*$scope.panel.pages)
+        .sort(sort);
+
+      $scope.populate_modal(request);
+
+      // Populate scope when we have results
+      request.doSearch().then(function(results) {
+        $scope.panelMeta.loading = false;
+
+        if(_segment === 0) {
+          $scope.panel.offset = 0;
+          $scope.hits = 0;
+          $scope.data = [];
+          $scope.current_fields = [];
+          query_id = $scope.query_id = new Date().getTime();
+        }
+
+        // Check for error and abort if found
+        if(!(_.isUndefined(results.error))) {
+          $scope.panel.error = $scope.parse_error(results.error);
+          return;
+        }
+
+        // Check that we're still on the same query, if not stop
+        if($scope.query_id === query_id) {
+
+          // This is exceptionally expensive, especially on events with a large number of fields
+          $scope.data = $scope.data.concat(_.map(results.hits.hits, function(hit) {
+            var
+              _h = _.clone(hit),
+              _p = _.omit(hit,'_source','sort','_score');
+
+            // _source is kind of a lie here, never display it, only select values from it
+            _h.kibana = {
+              _source : _.extend(kbn.flatten_json(hit._source),_p),
+              highlight : kbn.flatten_json(hit.highlight||{})
+            };
+
+            // Kind of cheating with the _.map here, but this is faster than kbn.get_all_fields
+            $scope.current_fields = $scope.current_fields.concat(_.keys(_h.kibana._source));
+
+            return _h;
+          }));
+
+          $scope.current_fields = _.uniq($scope.current_fields);
+          $scope.hits += results.hits.total;
+
+          // Sort the data
+          $scope.data = _.sortBy($scope.data, function(v){
+            if(!_.isUndefined(v.sort)) {
+              return v.sort[0];
+            } else {
+              return v._score;
+            }
+          });
+
+          // Reverse if needed
+          if($scope.panel.sort[1] === 'desc') {
+            $scope.data.reverse();
+          }
+
+          // Keep only what we need for the set
+          $scope.data = $scope.data.slice(0,$scope.panel.size * $scope.panel.pages);
+
+        } else {
+          return;
+        }
+
+        // If we're not sorting in reverse chrono order, query every index for
+        // size*pages results
+        // Otherwise, only get size*pages results then stop querying
+        if (($scope.data.length < $scope.panel.size*$scope.panel.pages ||
+          !((_.contains(filterSrv.timeField(),$scope.panel.sort[0])) && $scope.panel.sort[1] === 'desc')) &&
+          _segment+1 < dashboard.indices.length) {
+          $scope.get_data(_segment+1,$scope.query_id);
+        }
+
+      });
+    };
+
+    $scope.populate_modal = function(request) {
+      $scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
+    };
+
+    $scope.without_kibana = function (row) {
+      var _c = _.clone(row);
+      delete _c.kibana;
+      return _c;
+    };
+
+    $scope.set_refresh = function (state) {
+      $scope.refresh = state;
+    };
+
+    $scope.close_edit = function() {
+      if($scope.refresh) {
+        $scope.get_data();
+      }
+      $scope.columns = [];
+      _.each($scope.panel.fields,function(field) {
+        $scope.columns[field] = true;
+      });
+      $scope.refresh =  false;
+    };
+
+    $scope.locate = function(obj, path) {
+      path = path.split('.');
+      var arrayPattern = /(.+)\[(\d+)\]/;
+      for (var i = 0; i < path.length; i++) {
+        var match = arrayPattern.exec(path[i]);
+        if (match) {
+          obj = obj[match[1]][parseInt(match[2],10)];
+        } else {
+          obj = obj[path[i]];
+        }
+      }
+      return obj;
+    };
+
+
+  });
+
+  // This also escapes some xml sequences
+  module.filter('tableHighlight', function() {
+    return function(text) {
+      if (!_.isUndefined(text) && !_.isNull(text) && text.toString().length > 0) {
+        return text.toString().
+          replace(/&/g, '&amp;').
+          replace(/</g, '&lt;').
+          replace(/>/g, '&gt;').
+          replace(/\r?\n/g, '<br/>').
+          replace(/@start-highlight@/g, '<code class="highlight">').
+          replace(/@end-highlight@/g, '</code>');
+      }
+      return '';
+    };
+  });
+
+  module.filter('tableTruncate', function() {
+    return function(text,length,factor) {
+      if (!_.isUndefined(text) && !_.isNull(text) && text.toString().length > 0) {
+        return text.length > length/factor ? text.substr(0,length/factor)+'...' : text;
+      }
+      return '';
+    };
+  });
+
+
+
+  module.filter('tableJson', function() {
+    var json;
+    return function(text,prettyLevel) {
+      if (!_.isUndefined(text) && !_.isNull(text) && text.toString().length > 0) {
+        json = angular.toJson(text,prettyLevel > 0 ? true : false);
+        json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+        if(prettyLevel > 1) {
+          /* jshint maxlen: false */
+          json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
+            var cls = 'number';
+            if (/^"/.test(match)) {
+              if (/:$/.test(match)) {
+                cls = 'key strong';
+              } else {
+                cls = '';
+              }
+            } else if (/true|false/.test(match)) {
+              cls = 'boolean';
+            } else if (/null/.test(match)) {
+              cls = 'null';
+            }
+            return '<span class="' + cls + '">' + match + '</span>';
+          });
+        }
+        return json;
+      }
+      return '';
+    };
+  });
+
+  // WIP
+  module.filter('tableLocalTime', function(){
+    return function(text,event) {
+      return moment(event.sort[1]).format("YYYY-MM-DDTHH:mm:ss.SSSZ");
+    };
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/05e188ba/opensoc-ui/lib/public/app/panels/table/pagination.html
----------------------------------------------------------------------
diff --git a/opensoc-ui/lib/public/app/panels/table/pagination.html b/opensoc-ui/lib/public/app/panels/table/pagination.html
new file mode 100755
index 0000000..8eee4cc
--- /dev/null
+++ b/opensoc-ui/lib/public/app/panels/table/pagination.html
@@ -0,0 +1,32 @@
+  <div class="editor-row">
+    <div class="section">
+      <div class="editor-option">
+        <h6>Show Controls</h6><input type="checkbox" ng-model="panel.paging" ng-checked="panel.paging">
+      </div>
+      <div class="editor-option">
+        <h6>Overflow</h6>
+        <select class="input-small" ng-model="panel.overflow" ng-options="f.value as f.key for f in [{key:'scroll',value:'height'},{key:'expand',value:'min-height'}]"></select>
+      </div>
+    </div>
+
+    <div class="section">
+      <div class="editor-option">
+        <h6>Per Page</h6>
+        <input type="number" class="input-mini" ng-model="panel.size" ng-change="get_data()">
+      </div>
+      <div class="editor-option">
+        <h6>&nbsp;</h6>
+        <center><i class='icon-remove'></i><center>
+      </div>
+      <div class="editor-option">
+        <h6>Page limit</h6>
+        <input type="number" class="input-mini" ng-model="panel.pages" ng-change="get_data()">
+      </div>
+      <div class="editor-option large">
+        <h6>Pageable</h6>
+        <strong class="large">= {{panel.size * panel.pages}}</strong>
+      </div>
+    </div>
+
+  </div>
+

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/05e188ba/opensoc-ui/lib/public/app/panels/terms/editor.html
----------------------------------------------------------------------
diff --git a/opensoc-ui/lib/public/app/panels/terms/editor.html b/opensoc-ui/lib/public/app/panels/terms/editor.html
new file mode 100755
index 0000000..ee849a6
--- /dev/null
+++ b/opensoc-ui/lib/public/app/panels/terms/editor.html
@@ -0,0 +1,70 @@
+  <div class="editor-row">
+    <div class="section">
+      <h5>Parameters</h5>
+      <div class="editor-option">
+        <label class="small">Terms mode</label>
+        <select class="input-medium" ng-model="panel.tmode" ng-options="f for f in ['terms','terms_stats']" ng-change="set_refresh(true)"></select>
+      </div>
+      <div class="editor-option" ng-show="panel.tmode == 'terms_stats'">
+        <label class="small">Stats type</label>
+        <select class="input-medium" ng-model="panel.tstat" ng-options="f for f in ['count', 'total_count', 'min', 'max', 'total', 'mean']"></select>
+      </div>
+      <div class="editor-option">
+        <label class="small">Field</label>
+        <input type="text" class="input-small" bs-typeahead="fields.list" ng-model="panel.field" ng-change="set_refresh(true)">
+      </div>
+      <div class="editor-option" ng-show="panel.tmode == 'terms_stats'">
+        <label class="small">Value field</label>
+        <input type="text" class="input-small" bs-typeahead="fields.list" ng-model="panel.valuefield" ng-change="set_refresh(true)">
+      </div>
+      <div class="editor-option">
+        <label class="small">Length</label>
+        <input class="input-small" type="number" ng-model="panel.size" ng-change="set_refresh(true)">
+      </div>
+      <div class="editor-option">
+        <label class="small">Order</label>
+        <select class="input-medium" ng-model="panel.order" ng-options="f for f in ['count','term','reverse_count','reverse_term']" ng-change="set_refresh(true)"  ng-show="panel.tmode == 'terms'"></select>
+        <select class="input-medium" ng-model="panel.order" ng-options="f for f in ['term', 'reverse_term', 'count', 'reverse_count', 'total', 'reverse_total', 'min', 'reverse_min', 'max', 'reverse_max', 'mean', 'reverse_mean']" ng-change="set_refresh(true)"  ng-show="panel.tmode == 'terms_stats'"></select>
+      </div>
+      <div class="editor-option" ng-show="panel.tmode == 'terms'">
+        <label class="small">Exclude Terms(s) (comma separated)</label>
+        <input array-join type="text" ng-model='panel.exclude'></input>
+      </div>
+    </div>
+  </div>
+  <div class="editor-row">
+    <div class="section">
+      <h5>View Options</h5>
+      <div class="editor-option">
+        <label class="small">Style</label>
+        <select class="input-small" ng-model="panel.chart" ng-options="f for f in ['bar','pie','table']"></select></span>
+      </div>
+      <div class="editor-option" ng-show="panel.chart == 'table'">
+        <label class="small">Font Size</label>
+        <select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['7pt','8pt','9pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select></span>
+      </div>
+      <div class="editor-option" ng-show="panel.chart == 'bar' || panel.chart == 'pie'">
+        <label class="small">Legend</label>
+        <select class="input-small" ng-model="panel.counter_pos" ng-options="f for f in ['above','below','none']"></select></span>
+      </div>
+      <div class="editor-option" ng-show="panel.chart != 'table' && panel.counter_pos != 'none'">
+        <label class="small" >Legend Format</label>
+        <select class="input-small" ng-model="panel.arrangement" ng-options="f for f in ['horizontal','vertical']"></select></span>
+      </div>
+      <div class="editor-option">
+        <label class="small">Missing</label><input type="checkbox" ng-model="panel.missing" ng-checked="panel.missing">
+      </div>
+      <div class="editor-option">
+        <label class="small">Other</label><input type="checkbox" ng-model="panel.other" ng-checked="panel.other">
+      </div>
+      <div class="editor-option" ng-show="panel.chart == 'pie'">
+        <label class="small">Donut</label><input type="checkbox" ng-model="panel.donut" ng-checked="panel.donut">
+      </div>
+      <div class="editor-option" ng-show="panel.chart == 'pie'">
+        <label class="small">Tilt</label><input type="checkbox" ng-model="panel.tilt" ng-checked="panel.tilt">
+      </div>
+      <div class="editor-option" ng-show="panel.chart == 'pie'">
+        <label class="small">Labels</label><input type="checkbox" ng-model="panel.labels" ng-checked="panel.labels">
+      </div>
+    </div>
+  </div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/05e188ba/opensoc-ui/lib/public/app/panels/terms/module.html
----------------------------------------------------------------------
diff --git a/opensoc-ui/lib/public/app/panels/terms/module.html b/opensoc-ui/lib/public/app/panels/terms/module.html
new file mode 100755
index 0000000..2adaa3e
--- /dev/null
+++ b/opensoc-ui/lib/public/app/panels/terms/module.html
@@ -0,0 +1,83 @@
+<div ng-controller='terms' ng-init="init()">
+  <style>
+    .pieLabel { pointer-events: none }
+    .terms-legend-term {
+      word-break: break-all;
+    }
+  </style>
+
+  <!-- START Pie or bar chart -->
+  <div ng-show="panel.counter_pos == 'above' && (panel.chart == 'bar' || panel.chart == 'pie')" id='{{$id}}-legend'>
+    <!-- vertical legend above -->
+    <table class="small" ng-show="panel.arrangement == 'vertical'">
+      <tr ng-repeat="term in legend">
+        <td><i class="icon-circle" ng-style="{color:term.color}"></i></td>
+        <td class="terms-legend-term" style="padding-right:10px;padding-left:10px;">{{term.label}}</td>
+        <td>{{term.data[0][1]}}</td>
+      </tr>
+    </table>
+
+    <!-- horizontal legend above -->
+    <span class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="term in legend" style="float:left;padding-left: 10px;">
+      <span>
+        <i class="icon-circle" ng-style="{color:term.color}"></i>
+        <span class="terms-legend-term">{{term.label}}</span> ({{term.data[0][1]}})
+      </span>
+    </span>
+
+    <span class="small pull-left" ng-show="panel.tmode == 'terms_stats'">
+      &nbsp | {{ panel.tstat }} of <strong>{{ panel.valuefield }}</strong>
+    </span>
+
+  </div>
+
+  <!-- keep legend from over lapping -->
+  <div style="clear:both"></div>
+
+  <div ng-show="panel.chart == 'pie' || panel.chart == 'bar'" terms-chart params="{{panel}}" style="position:relative" class="pointer"></div>
+
+  <div ng-show="panel.counter_pos == 'below' && (panel.chart == 'bar' || panel.chart == 'pie')" id='{{$id}}-legend'>
+    <!-- vertical legend below -->
+    <table class="small" ng-show="panel.arrangement == 'vertical'">
+      <tr ng-repeat="term in legend">
+        <td><i class="icon-circle" ng-style="{color:term.color}"></i></i></td>
+        <td class="terms-legend-term" style="padding-right:10px;padding-left:10px;">{{term.label}}</td>
+        <td>{{term.data[0][1]}}</td>
+      </tr>
+    </table>
+
+    <!-- horizontal legend below -->
+    <span class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="term in legend" style="float:left;padding-left: 10px;">
+      <span>
+        <i class="icon-circle" ng-style="{color:term.color}"></i>
+        <span class="terms-legend-term">{{term.label}}</span> ({{term.data[0][1]}})
+      </span>
+    </span>
+
+    <span class="small pull-left" ng-show="panel.tmode == 'terms_stats'">
+      &nbsp | {{ panel.tstat }} of <strong>{{ panel.valuefield }}</strong>
+    </span>
+
+    <div style="clear:both"></div>
+  </div>
+  <!-- END Pie or Bar chart -->
+
+
+
+  <table ng-style="panel.style" class="table table-striped table-condensed" ng-show="panel.chart == 'table'">
+    <thead>
+      <th>Term</th> <th>{{ panel.tmode == 'terms_stats' ? panel.tstat : 'Count' }}</th> <th>Action</th>
+    </thead>
+    <tr ng-repeat="term in data" ng-show="showMeta(term)">
+      <td class="terms-legend-term">{{term.label}}</td>
+      <td>{{term.data[0][1]}}</td>
+      <td>
+        <span ng-hide="term.meta == 'other'">
+          <i class='icon-search pointer' ng-click="build_search(term)"></i>
+          <i class='icon-ban-circle pointer' ng-click="build_search(term,true)"></i>
+        </span>
+      </td>
+    </tr>
+  </table>
+
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/05e188ba/opensoc-ui/lib/public/app/panels/terms/module.js
----------------------------------------------------------------------
diff --git a/opensoc-ui/lib/public/app/panels/terms/module.js b/opensoc-ui/lib/public/app/panels/terms/module.js
new file mode 100755
index 0000000..edb7735
--- /dev/null
+++ b/opensoc-ui/lib/public/app/panels/terms/module.js
@@ -0,0 +1,413 @@
+/** @scratch /panels/5
+ *
+ * include::panels/terms.asciidoc[]
+ */
+
+/** @scratch /panels/terms/0
+ *
+ * == terms
+ * Status: *Stable*
+ *
+ * A table, bar chart or pie chart based on the results of an Elasticsearch terms facet.
+ *
+ */
+define([
+  'angular',
+  'app',
+  'lodash',
+  'jquery',
+  'kbn'
+],
+function (angular, app, _, $, kbn) {
+  'use strict';
+
+  var module = angular.module('kibana.panels.terms', []);
+  app.useModule(module);
+
+  module.controller('terms', function($scope, querySrv, dashboard, filterSrv, fields) {
+    $scope.panelMeta = {
+      modals : [
+        {
+          description: "Inspect",
+          icon: "icon-info-sign",
+          partial: "app/partials/inspector.html",
+          show: $scope.panel.spyable
+        }
+      ],
+      editorTabs : [
+        {title:'Queries', src:'app/partials/querySelect.html'}
+      ],
+      status  : "Stable",
+      description : "Displays the results of an elasticsearch facet as a pie chart, bar chart, or a "+
+        "table"
+    };
+
+    // Set and populate defaults
+    var _d = {
+      /** @scratch /panels/terms/5
+       * === Parameters
+       *
+       * field:: The field on which to computer the facet
+       */
+      field   : '_type',
+      /** @scratch /panels/terms/5
+       * exclude:: terms to exclude from the results
+       */
+      exclude : [],
+      /** @scratch /panels/terms/5
+       * missing:: Set to false to disable the display of a counter showing how much results are
+       * missing the field
+       */
+      missing : true,
+      /** @scratch /panels/terms/5
+       * other:: Set to false to disable the display of a counter representing the aggregate of all
+       * values outside of the scope of your +size+ property
+       */
+      other   : true,
+      /** @scratch /panels/terms/5
+       * size:: Show this many terms
+       */
+      size    : 10,
+      /** @scratch /panels/terms/5
+       * order:: In terms mode: count, term, reverse_count or reverse_term,
+       * in terms_stats mode: term, reverse_term, count, reverse_count,
+       * total, reverse_total, min, reverse_min, max, reverse_max, mean or reverse_mean
+       */
+      order   : 'count',
+      style   : { "font-size": '10pt'},
+      /** @scratch /panels/terms/5
+       * donut:: In pie chart mode, draw a hole in the middle of the pie to make a tasty donut.
+       */
+      donut   : false,
+      /** @scratch /panels/terms/5
+       * tilt:: In pie chart mode, tilt the chart back to appear as more of an oval shape
+       */
+      tilt    : false,
+      /** @scratch /panels/terms/5
+       * lables:: In pie chart mode, draw labels in the pie slices
+       */
+      labels  : true,
+      /** @scratch /panels/terms/5
+       * arrangement:: In bar or pie mode, arrangement of the legend. horizontal or vertical
+       */
+      arrangement : 'horizontal',
+      /** @scratch /panels/terms/5
+       * chart:: table, bar or pie
+       */
+      chart       : 'bar',
+      /** @scratch /panels/terms/5
+       * counter_pos:: The location of the legend in respect to the chart, above, below, or none.
+       */
+      counter_pos : 'above',
+      /** @scratch /panels/terms/5
+       * spyable:: Set spyable to false to disable the inspect button
+       */
+      spyable     : true,
+      /** @scratch /panels/terms/5
+       *
+       * ==== Queries
+       * queries object:: This object describes the queries to use on this panel.
+       * queries.mode::: Of the queries available, which to use. Options: +all, pinned, unpinned, selected+
+       * queries.ids::: In +selected+ mode, which query ids are selected.
+       */
+      queries     : {
+        mode        : 'all',
+        ids         : []
+      },
+
+      /*
+       * locked:: whether to lock the query, preventing it from being affected by filters
+       */
+      locked: false,
+      /** @scratch /panels/terms/5
+       * tmode:: Facet mode: terms or terms_stats
+       */
+      tmode       : 'terms',
+      /** @scratch /panels/terms/5
+       * tstat:: Terms_stats facet stats field
+       */
+      tstat       : 'total',
+      /** @scratch /panels/terms/5
+       * valuefield:: Terms_stats facet value field
+       */
+      valuefield  : ''
+    };
+
+    _.defaults($scope.panel,_d);
+
+    $scope.init = function () {
+      $scope.hits = 0;
+
+      $scope.$on('refresh',function(){
+        $scope.get_data();
+      });
+      $scope.get_data();
+
+    };
+
+    $scope.get_data = function() {
+      // Make sure we have everything for the request to complete
+      if(dashboard.indices.length === 0) {
+        return;
+      }
+
+      $scope.panelMeta.loading = true;
+      var request,
+        builder,
+        results,
+        boolQuery,
+        queries;
+
+      $scope.field = _.contains(fields.list,$scope.panel.field+'.raw') ?
+        $scope.panel.field+'.raw' : $scope.panel.field;
+
+      request = $scope.ejs.Request().indices(dashboard.indices);
+
+      $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
+      queries = querySrv.getQueryObjs($scope.panel.queries.ids);
+
+      // This could probably be changed to a BoolFilter
+      boolQuery = $scope.ejs.BoolQuery();
+      _.each(queries,function(q) {
+        boolQuery = boolQuery.should(querySrv.toEjsObj(q));
+      });
+
+      // Terms mode
+      if($scope.panel.tmode === 'terms') {
+        builder = $scope.ejs.TermsFacet('terms')
+          .field($scope.field)
+          .size($scope.panel.size)
+          .order($scope.panel.order)
+          .exclude($scope.panel.exclude);
+      }
+      if($scope.panel.tmode === 'terms_stats') {
+        builder = $scope.ejs.TermStatsFacet('terms')
+          .valueField($scope.panel.valuefield)
+          .keyField($scope.field)
+          .size($scope.panel.size)
+          .order($scope.panel.order);
+      }
+
+      builder = builder.facetFilter($scope.ejs.QueryFilter(
+        $scope.ejs.FilteredQuery(
+          boolQuery,
+          $scope.panel.locked ? null : filterSrv.getBoolFilter(filterSrv.ids())
+        )));
+
+      request = request.facet(builder).size(0);
+
+      // Populate the inspector panel
+      $scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
+
+      results = request.doSearch();
+
+      // Populate scope when we have results
+      results.then(function(results) {
+        $scope.panelMeta.loading = false;
+        if($scope.panel.tmode === 'terms') {
+          $scope.hits = results.hits.total;
+        }
+
+        $scope.results = results;
+
+        $scope.$emit('render');
+      });
+    };
+
+    $scope.build_search = function(term,negate) {
+      if(_.isUndefined(term.meta)) {
+        filterSrv.set({type:'terms',field:$scope.field,value:term.label,
+          mandate:(negate ? 'mustNot':'must')});
+      } else if(term.meta === 'missing') {
+        filterSrv.set({type:'exists',field:$scope.field,
+          mandate:(negate ? 'must':'mustNot')});
+      } else {
+        return;
+      }
+    };
+
+    $scope.set_refresh = function (state) {
+      $scope.refresh = state;
+    };
+
+    $scope.close_edit = function() {
+      if($scope.refresh) {
+        $scope.get_data();
+      }
+      $scope.refresh =  false;
+      $scope.$emit('render');
+    };
+
+    $scope.showMeta = function(term) {
+      if(_.isUndefined(term.meta)) {
+        return true;
+      }
+      if(term.meta === 'other' && !$scope.panel.other) {
+        return false;
+      }
+      if(term.meta === 'missing' && !$scope.panel.missing) {
+        return false;
+      }
+      return true;
+    };
+
+  });
+
+  module.directive('termsChart', function(querySrv) {
+    return {
+      restrict: 'A',
+      link: function(scope, elem) {
+
+        // Receive render events
+        scope.$on('render',function(){
+          render_panel();
+        });
+
+        // Re-render if the window is resized
+        angular.element(window).bind('resize', function(){
+          render_panel();
+        });
+
+        function build_results() {
+          var k = 0;
+          scope.data = [];
+          _.each(scope.results.facets.terms.terms, function(v) {
+            var slice;
+            if(scope.panel.tmode === 'terms') {
+              slice = { label : v.term, data : [[k,v.count]], actions: true};
+            }
+            if(scope.panel.tmode === 'terms_stats') {
+              slice = { label : v.term, data : [[k,v[scope.panel.tstat]]], actions: true};
+            }
+            scope.data.push(slice);
+            k = k + 1;
+          });
+
+          scope.data.push({label:'Missing field',
+            data:[[k,scope.results.facets.terms.missing]],meta:"missing",color:'#aaa',opacity:0});
+
+          if(scope.panel.tmode === 'terms') {
+            scope.data.push({label:'Other values',
+              data:[[k+1,scope.results.facets.terms.other]],meta:"other",color:'#444'});
+          }
+        }
+
+        // Function for rendering panel
+        function render_panel() {
+          var plot, chartData;
+
+          build_results();
+
+          // IE doesn't work without this
+          elem.css({height:scope.row.height});
+
+          // Make a clone we can operate on.
+          chartData = _.clone(scope.data);
+          chartData = scope.panel.missing ? chartData :
+            _.without(chartData,_.findWhere(chartData,{meta:'missing'}));
+          chartData = scope.panel.other ? chartData :
+          _.without(chartData,_.findWhere(chartData,{meta:'other'}));
+
+          // Populate element.
+          require(['jquery.flot.pie'], function(){
+            // Populate element
+            try {
+              // Add plot to scope so we can build out own legend
+              if(scope.panel.chart === 'bar') {
+                plot = $.plot(elem, chartData, {
+                  legend: { show: false },
+                  series: {
+                    lines:  { show: false, },
+                    bars:   { show: true,  fill: 1, barWidth: 0.8, horizontal: false },
+                    shadowSize: 1
+                  },
+                  yaxis: { show: true, min: 0, color: "#c8c8c8" },
+                  xaxis: { show: false },
+                  grid: {
+                    borderWidth: 0,
+                    borderColor: '#c8c8c8',
+                    color: "#c8c8c8",
+                    hoverable: true,
+                    clickable: true
+                  },
+                  colors: querySrv.colors
+                });
+              }
+              if(scope.panel.chart === 'pie') {
+                var labelFormat = function(label, series){
+                  return '<div ng-click="build_search(panel.field,\''+label+'\')'+
+                    ' "style="font-size:8pt;text-align:center;padding:2px;color:white;">'+
+                    label+'<br/>'+Math.round(series.percent)+'%</div>';
+                };
+
+                plot = $.plot(elem, chartData, {
+                  legend: { show: false },
+                  series: {
+                    pie: {
+                      innerRadius: scope.panel.donut ? 0.4 : 0,
+                      tilt: scope.panel.tilt ? 0.45 : 1,
+                      radius: 1,
+                      show: true,
+                      combine: {
+                        color: '#999',
+                        label: 'The Rest'
+                      },
+                      stroke: {
+                        width: 0
+                      },
+                      label: {
+                        show: scope.panel.labels,
+                        radius: 2/3,
+                        formatter: labelFormat,
+                        threshold: 0.1
+                      }
+                    }
+                  },
+                  //grid: { hoverable: true, clickable: true },
+                  grid:   { hoverable: true, clickable: true, color: '#c8c8c8' },
+                  colors: querySrv.colors
+                });
+              }
+
+              // Populate legend
+              if(elem.is(":visible")){
+                setTimeout(function(){
+                  scope.legend = plot.getData();
+                  if(!scope.$$phase) {
+                    scope.$apply();
+                  }
+                });
+              }
+
+            } catch(e) {
+              elem.text(e);
+            }
+          });
+        }
+
+        elem.bind("plotclick", function (event, pos, object) {
+          if(object) {
+            scope.build_search(scope.data[object.seriesIndex]);
+          }
+        });
+
+        var $tooltip = $('<div>');
+        elem.bind("plothover", function (event, pos, item) {
+          if (item) {
+            var value = scope.panel.chart === 'bar' ? item.datapoint[1] : item.datapoint[1][0][1];
+            $tooltip
+              .html(
+                kbn.query_color_dot(item.series.color, 20) + ' ' +
+                item.series.label + " (" + value.toFixed(0)+")"
+              )
+              .place_tt(pos.pageX, pos.pageY);
+          } else {
+            $tooltip.remove();
+          }
+        });
+
+      }
+    };
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/05e188ba/opensoc-ui/lib/public/app/panels/test-chart/module.js
----------------------------------------------------------------------
diff --git a/opensoc-ui/lib/public/app/panels/test-chart/module.js b/opensoc-ui/lib/public/app/panels/test-chart/module.js
new file mode 100644
index 0000000..e0424ca
--- /dev/null
+++ b/opensoc-ui/lib/public/app/panels/test-chart/module.js
@@ -0,0 +1,414 @@
+/** @scratch /panels/5
+ *
+ * include::panels/test.asciidoc[]
+ */
+
+/** @scratch /panels/test/0
+ *
+ * == terms
+ * Status: *Stable*
+ *
+ * A pie chart based on the results of an ES.
+ *
+ */
+define([
+  'angular',
+  'app',
+  'lodash',
+  'jquery',
+  'kbn'
+],
+function (angular, app, _, $, kbn) {
+  'use strict';
+
+  var module = angular.module('kibana.panels.tests', []);
+  app.useModule(module);
+
+  module.controller('tests', function($scope, querySrv, dashboard, filterSrv, fields) {
+    $scope.panelMeta = {
+      modals : [
+        {
+          description: "Inspect",
+          icon: "icon-info-sign",
+          partial: "app/partials/inspector.html",
+          show: $scope.panel.spyable
+        }
+      ],
+      editorTabs : [
+        {title:'Queries', src:'app/partials/querySelect.html'}
+      ],
+      status  : "Stable",
+      description : "Displays the results of an elasticsearch facet as a pie chart, bar chart, or a "+
+        "table"
+    };
+
+    // Set and populate defaults
+    var _d = {
+      /** @scratch /panels/terms/5
+       * === Parameters
+       *
+       * field:: The field on which to computer the facet
+       */
+      field   : '_type',
+      /** @scratch /panels/terms/5
+       * exclude:: terms to exclude from the results
+       */
+      exclude : [],
+      /** @scratch /panels/terms/5
+       * missing:: Set to false to disable the display of a counter showing how much results are
+       * missing the field
+       */
+      missing : true,
+      /** @scratch /panels/terms/5
+       * other:: Set to false to disable the display of a counter representing the aggregate of all
+       * values outside of the scope of your +size+ property
+       */
+      other   : true,
+      /** @scratch /panels/terms/5
+       * size:: Show this many terms
+       */
+      size    : 10,
+      /** @scratch /panels/terms/5
+       * order:: In terms mode: count, term, reverse_count or reverse_term,
+       * in terms_stats mode: term, reverse_term, count, reverse_count,
+       * total, reverse_total, min, reverse_min, max, reverse_max, mean or reverse_mean
+       */
+      order   : 'count',
+      style   : { "font-size": '10pt'},
+      /** @scratch /panels/terms/5
+       * donut:: In pie chart mode, draw a hole in the middle of the pie to make a tasty donut.
+       */
+      donut   : false,
+      /** @scratch /panels/terms/5
+       * tilt:: In pie chart mode, tilt the chart back to appear as more of an oval shape
+       */
+      tilt    : false,
+      /** @scratch /panels/terms/5
+       * lables:: In pie chart mode, draw labels in the pie slices
+       */
+      labels  : true,
+      /** @scratch /panels/terms/5
+       * arrangement:: In bar or pie mode, arrangement of the legend. horizontal or vertical
+       */
+      arrangement : 'horizontal',
+      /** @scratch /panels/terms/5
+       * chart:: table, bar or pie
+       */
+      chart       : 'bar',
+      /** @scratch /panels/terms/5
+       * counter_pos:: The location of the legend in respect to the chart, above, below, or none.
+       */
+      counter_pos : 'above',
+      /** @scratch /panels/terms/5
+       * spyable:: Set spyable to false to disable the inspect button
+       */
+      spyable     : true,
+      /** @scratch /panels/terms/5
+       *
+       * ==== Queries
+       * queries object:: This object describes the queries to use on this panel.
+       * queries.mode::: Of the queries available, which to use. Options: +all, pinned, unpinned, selected+
+       * queries.ids::: In +selected+ mode, which query ids are selected.
+       */
+      queries     : {
+        mode        : 'all',
+        ids         : []
+      },
+
+      /*
+       * locked:: whether to lock the query, preventing it from being affected by filters
+       */
+      locked: false,
+      /** @scratch /panels/terms/5
+       * tmode:: Facet mode: terms or terms_stats
+       */
+      tmode       : 'terms',
+      /** @scratch /panels/terms/5
+       * tstat:: Terms_stats facet stats field
+       */
+      tstat       : 'total',
+      /** @scratch /panels/terms/5
+       * valuefield:: Terms_stats facet value field
+       */
+      valuefield  : ''
+    };
+
+    _.defaults($scope.panel,_d);
+
+    $scope.init = function () {
+      $scope.hits = 0;
+
+      // Static Chart
+      // $scope.$on('refresh',function(){
+      //   $scope.get_data();
+      // });
+      $scope.get_data();
+
+    };
+
+    $scope.get_data = function() {
+      // Make sure we have everything for the request to complete
+      if(dashboard.indices.length === 0) {
+        return;
+      }
+
+      $scope.panelMeta.loading = true;
+      var request,
+        builder,
+        results,
+        boolQuery,
+        queries;
+
+      $scope.field = _.contains(fields.list,$scope.panel.field+'.raw') ?
+        $scope.panel.field+'.raw' : $scope.panel.field;
+
+      request = $scope.ejs.Request().indices(dashboard.indices);
+
+      $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
+      queries = querySrv.getQueryObjs($scope.panel.queries.ids);
+
+      // This could probably be changed to a BoolFilter
+      boolQuery = $scope.ejs.BoolQuery();
+      _.each(queries,function(q) {
+        boolQuery = boolQuery.should(querySrv.toEjsObj(q));
+      });
+
+      // Terms mode
+      if($scope.panel.tmode === 'terms') {
+        builder = $scope.ejs.TermsFacet('terms')
+          .field($scope.field)
+          .size($scope.panel.size)
+          .order($scope.panel.order)
+          .exclude($scope.panel.exclude);
+      }
+      if($scope.panel.tmode === 'terms_stats') {
+        builder = $scope.ejs.TermStatsFacet('terms')
+          .valueField($scope.panel.valuefield)
+          .keyField($scope.field)
+          .size($scope.panel.size)
+          .order($scope.panel.order);
+      }
+
+      builder = builder.facetFilter($scope.ejs.QueryFilter(
+        $scope.ejs.FilteredQuery(
+          boolQuery,
+          $scope.panel.locked ? null : filterSrv.getBoolFilter(filterSrv.ids())
+        )));
+
+      request = request.facet(builder).size(0);
+
+      // Populate the inspector panel
+      $scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
+
+      results = request.doSearch();
+
+      // Populate scope when we have results
+      results.then(function(results) {
+        $scope.panelMeta.loading = false;
+        if($scope.panel.tmode === 'terms') {
+          $scope.hits = results.hits.total;
+        }
+
+        $scope.results = results;
+
+        $scope.$emit('render');
+      });
+    };
+
+    $scope.build_search = function(term,negate) {
+      if(_.isUndefined(term.meta)) {
+        filterSrv.set({type:'terms',field:$scope.field,value:term.label,
+          mandate:(negate ? 'mustNot':'must')});
+      } else if(term.meta === 'missing') {
+        filterSrv.set({type:'exists',field:$scope.field,
+          mandate:(negate ? 'must':'mustNot')});
+      } else {
+        return;
+      }
+    };
+
+    $scope.set_refresh = function (state) {
+      $scope.refresh = state;
+    };
+
+    $scope.close_edit = function() {
+      if($scope.refresh) {
+        $scope.get_data();
+      }
+      $scope.refresh =  false;
+      $scope.$emit('render');
+    };
+
+    $scope.showMeta = function(term) {
+      if(_.isUndefined(term.meta)) {
+        return true;
+      }
+      if(term.meta === 'other' && !$scope.panel.other) {
+        return false;
+      }
+      if(term.meta === 'missing' && !$scope.panel.missing) {
+        return false;
+      }
+      return true;
+    };
+
+  });
+
+  module.directive('termsChart', function(querySrv) {
+    return {
+      restrict: 'A',
+      link: function(scope, elem) {
+
+        // Receive render events
+        scope.$on('render',function(){
+          render_panel();
+        });
+
+        // Re-render if the window is resized
+        angular.element(window).bind('resize', function(){
+          render_panel();
+        });
+
+        function build_results() {
+          var k = 0;
+          scope.data = [];
+          _.each(scope.results.facets.terms.terms, function(v) {
+            var slice;
+            if(scope.panel.tmode === 'terms') {
+              slice = { label : v.term, data : [[k,v.count]], actions: true};
+            }
+            if(scope.panel.tmode === 'terms_stats') {
+              slice = { label : v.term, data : [[k,v[scope.panel.tstat]]], actions: true};
+            }
+            scope.data.push(slice);
+            k = k + 1;
+          });
+
+          scope.data.push({label:'Missing field',
+            data:[[k,scope.results.facets.terms.missing]],meta:"missing",color:'#aaa',opacity:0});
+
+          if(scope.panel.tmode === 'terms') {
+            scope.data.push({label:'Other values',
+              data:[[k+1,scope.results.facets.terms.other]],meta:"other",color:'#444'});
+          }
+        }
+
+        // Function for rendering panel
+        function render_panel() {
+          var plot, chartData;
+
+          build_results();
+
+          // IE doesn't work without this
+          elem.css({height:scope.row.height});
+
+          // Make a clone we can operate on.
+          chartData = _.clone(scope.data);
+          chartData = scope.panel.missing ? chartData :
+            _.without(chartData,_.findWhere(chartData,{meta:'missing'}));
+          chartData = scope.panel.other ? chartData :
+          _.without(chartData,_.findWhere(chartData,{meta:'other'}));
+
+          // Populate element.
+          require(['jquery.flot.pie'], function(){
+            // Populate element
+            try {
+              // Add plot to scope so we can build out own legend
+              if(scope.panel.chart === 'bar') {
+                plot = $.plot(elem, chartData, {
+                  legend: { show: false },
+                  series: {
+                    lines:  { show: false, },
+                    bars:   { show: true,  fill: 1, barWidth: 0.8, horizontal: false },
+                    shadowSize: 1
+                  },
+                  yaxis: { show: true, min: 0, color: "#c8c8c8" },
+                  xaxis: { show: false },
+                  grid: {
+                    borderWidth: 0,
+                    borderColor: '#c8c8c8',
+                    color: "#c8c8c8",
+                    hoverable: true,
+                    clickable: true
+                  },
+                  colors: querySrv.colors
+                });
+              }
+              if(scope.panel.chart === 'pie') {
+                var labelFormat = function(label, series){
+                  return '<div ng-click="build_search(panel.field,\''+label+'\')'+
+                    ' "style="font-size:8pt;text-align:center;padding:2px;color:white;">'+
+                    label+'<br/>'+Math.round(series.percent)+'%</div>';
+                };
+
+                plot = $.plot(elem, chartData, {
+                  legend: { show: false },
+                  series: {
+                    pie: {
+                      innerRadius: scope.panel.donut ? 0.4 : 0,
+                      tilt: scope.panel.tilt ? 0.45 : 1,
+                      radius: 1,
+                      show: true,
+                      combine: {
+                        color: '#999',
+                        label: 'The Rest'
+                      },
+                      stroke: {
+                        width: 0
+                      },
+                      label: {
+                        show: scope.panel.labels,
+                        radius: 2/3,
+                        formatter: labelFormat,
+                        threshold: 0.1
+                      }
+                    }
+                  },
+                  //grid: { hoverable: true, clickable: true },
+                  grid:   { hoverable: true, clickable: true, color: '#fff' },
+                  colors: querySrv.colors
+                });
+              }
+
+              // Populate legend
+              if(elem.is(":visible")){
+                setTimeout(function(){
+                  scope.legend = plot.getData();
+                  if(!scope.$$phase) {
+                    scope.$apply();
+                  }
+                });
+              }
+
+            } catch(e) {
+              elem.text(e);
+            }
+          });
+        }
+
+        elem.bind("plotclick", function (event, pos, object) {
+          if(object) {
+            scope.build_search(scope.data[object.seriesIndex]);
+          }
+        });
+
+        var $tooltip = $('<div>');
+        elem.bind("plothover", function (event, pos, item) {
+          if (item) {
+            var value = scope.panel.chart === 'bar' ? item.datapoint[1] : item.datapoint[1][0][1];
+            $tooltip
+              .html(
+                kbn.query_color_dot(item.series.color, 20) + ' ' +
+                item.series.label + " (" + value.toFixed(0)+")"
+              )
+              .place_tt(pos.pageX, pos.pageY);
+          } else {
+            $tooltip.remove();
+          }
+        });
+
+      }
+    };
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/05e188ba/opensoc-ui/lib/public/app/panels/text/editor.html
----------------------------------------------------------------------
diff --git a/opensoc-ui/lib/public/app/panels/text/editor.html b/opensoc-ui/lib/public/app/panels/text/editor.html
new file mode 100755
index 0000000..02177bc
--- /dev/null
+++ b/opensoc-ui/lib/public/app/panels/text/editor.html
@@ -0,0 +1,16 @@
+<div>
+  <div class="row-fluid">
+    <div class="span4">
+      <label class="small">Mode</label> <select class="input-medium" ng-model="panel.mode" ng-options="f for f in ['html','markdown','text']"></select>
+    </div>
+    <div class="span2" ng-show="panel.mode == 'text'">
+      <label class="small">Font Size</label> <select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['6pt','7pt','8pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select>
+    </div>
+  </div>
+
+  <label class=small>Content 
+    <span ng-show="panel.mode == 'html'">(This area uses HTML sanitized via AngularJS's <a href='http://docs.angularjs.org/api/ngSanitize.$sanitize'>$sanitize</a> service)</span>
+    <span ng-show="panel.mode == 'markdown'">(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)</span>
+  </label>
+  <textarea ng-model="panel.content" rows="6" style="width:95%"></textarea>
+</div>
\ No newline at end of file