You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by si...@apache.org on 2016/01/14 18:03:03 UTC

[08/85] [partial] incubator-metron git commit: Rename all OpenSOC files to Metron

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/dasheditor.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/dasheditor.html b/metron-ui/lib/public/app/partials/dasheditor.html
new file mode 100755
index 0000000..e9aaa8c
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/dasheditor.html
@@ -0,0 +1,187 @@
+<div class="modal-body">
+  <div class="pull-right editor-title">Dashboard settings</div>
+
+  <div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
+    <div ng-repeat="tab in ['General','Index','Rows','Controls']" data-title="{{tab}}">
+    </div>
+    <div ng-repeat="tab in dashboard.current.nav|editable" data-title="{{tab.title || tab.type}}">
+    </div>
+  </div>
+
+  <div ng-if="editor.index == 0">
+    <div class="editor-row">
+      <div class="section">
+        <div class="editor-option">
+          <label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.current.title'></input>
+        </div>
+        <div class="editor-option">
+          <label class="small">Style</label><select class="input-small" ng-model="dashboard.current.style" ng-options="f for f in ['dark','light']"></select>
+        </div>
+        <div class="editor-option">
+          <label class="small"> Editable </label><input type="checkbox" ng-model="dashboard.current.editable" ng-checked="dashboard.current.editable" />
+        </div>
+        <div class="editor-option">
+          <label class="small"> Hints <tip>Show 'Add panel' hints in empty spaces</tip></label><input type="checkbox" ng-model="dashboard.current.panel_hints" ng-checked="dashboard.current.panel_hints" />
+        </div>
+        <!-- <div class="editor-option">
+          <label class="small"> Real-Time <tip>Enables real-time alerts</tip></label><input type="checkbox" ng-model="" ng-checked="dashboard.current.realtime" />
+        </div> -->
+      </div>
+    </div>
+  </div>
+  <div ng-if="editor.index == 1">
+    <div class="editor-row">
+      <div class="section">
+        <h5>Index Settings</h5>
+        <div ng-show="dashboard.current.index.interval != 'none'" class="row-fluid">
+           <div class="editor-option">
+            <p class="small">
+              Time stamped indices use your selected time range to create a list of
+              indices that match a specified timestamp pattern. This can be very
+              efficient for some data sets (eg, logs) For example, to match the
+              default logstash index pattern you might use
+              <code>[logstash-]YYYY.MM.DD</code>. The [] in "[logstash-]" are
+              important as they instruct Kibana not to treat those letters as a
+              pattern. You may also specify multiple indices by seperating them with a comma(,).
+              For example <code>[web-]YYYY.MM.DD,[mail-]YYYY.MM.DD</code>
+              Please also note that indices should rollover at midnight <strong>UTC</strong>.
+            </p>
+            <p class="small">
+              See <a href="http://momentjs.com/docs/#/displaying/format/">http://momentjs.com/docs/#/displaying/format/</a>
+              for documentation on date formatting.
+            </p>
+           </div>
+         </div>
+      </div>
+    </div>
+    <div class="editor-row">
+      <div class="section">
+        <div class="editor-option">
+          <h6>Timestamping</h6><select class="input-small" ng-model="dashboard.current.index.interval" ng-options="f for f in ['none','hour','day','week','month','year']"></select>
+        </div>
+        <div class="editor-option" ng-show="dashboard.current.index.interval != 'none'">
+          <h6>Index pattern <small>Absolutes in []</small></h6>
+          <input type="text" class="input-large" ng-model="dashboard.current.index.pattern">
+        </div>
+        <div class="editor-option" ng-show="dashboard.current.index.interval != 'none'">
+          <h6>Failover <i class="icon-question-sign" bs-tooltip="'If no indices match the pattern, failover to default index *NOT RECOMMENDED*'"></i></h6>
+          <input type="checkbox" ng-model="dashboard.current.failover" ng-checked="dashboard.current.failover" />
+        </div>
+        <div class="editor-option" ng-show="dashboard.current.failover || dashboard.current.index.interval == 'none'">
+          <h6>Default Index <small ng-show="dashboard.current.index.interval != 'none'">If index not found</small></h6>
+          <input type="text" class="input-medium" ng-model="dashboard.current.index.default">
+        </div>
+        <div class="editor-option">
+          <h6>Preload Fields <i class="icon-question-sign" bs-tooltip="'Preload available fields for the purpose of autocomplete. Turn this off if you have many fields'"></i></h6>
+          <input type="checkbox" ng-model="dashboard.current.index.warm_fields" ng-checked="dashboard.current.index.warm_fields" />
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div ng-if="editor.index == 2">
+    <div class="row-fluid">
+      <div class="span8">
+        <h4>Rows</h4>
+        <table class="table table-striped">
+          <thead>
+            <th width="1%"></th>
+            <th width="1%"></th>
+            <th width="1%"></th>
+            <th width="97%">Title</th>
+          </thead>
+          <tr ng-repeat="row in dashboard.current.rows">
+            <td><i ng-click="_.move(dashboard.current.rows,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
+            <td><i ng-click="_.move(dashboard.current.rows,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
+            <td><i ng-click="dashboard.current.rows = _.without(dashboard.current.rows,row)" class="pointer icon-remove"></i></td>
+            <td>{{row.title||'Untitled'}}</td>
+          </tr>
+        </table>
+      </div>
+      <div class="span4">
+        <h4>Add Row</h4>
+        <label class="small">Title</label>
+        <input type="text" class="input-medium" ng-model='row.title' placeholder="New row"></input>
+        <label class="small">Height</label>
+        <input type="text" class="input-mini" ng-model='row.height'></input>
+      </div>
+    </div>
+    <div class="row-fluid">
+
+    </div>
+  </div>
+
+  <div ng-if="editor.index == 3" ng-controller="dashLoader">
+    <div class="editor-row">
+      <div class="section">
+        <h5>Save to</h5>
+        <div class="editor-option">
+          <label class="small">Export</label><input type="checkbox" ng-model="dashboard.current.loader.save_local" ng-checked="dashboard.current.loader.save_local">
+        </div>
+        <div class="editor-option">
+          <label class="small">Browser</label><input type="checkbox" ng-model="dashboard.current.loader.save_default" ng-checked="dashboard.current.loader.save_default">
+        </div>
+        <div class="editor-option">
+          <label class="small">Gist <tip>Requires your domain to be OAUTH registered with Github<tip></label><input type="checkbox" ng-model="dashboard.current.loader.save_gist" ng-checked="dashboard.current.loader.save_gist">
+        </div>
+        <div class="editor-option">
+          <label class="small">Elasticsearch</label><input type="checkbox" ng-model="dashboard.current.loader.save_elasticsearch" ng-checked="dashboard.current.loader.save_elasticsearch">
+        </div>
+      </div>
+      <div class="section">
+        <h5>Load from</h5>
+        <div class="editor-option">
+          <label class="small">Local file</label><input type="checkbox" ng-model="dashboard.current.loader.load_local" ng-checked="dashboard.current.loader.load_local">
+        </div>
+        <div class="editor-option">
+          <label class="small">Gist</label><input type="checkbox" ng-model="dashboard.current.loader.load_gist" ng-checked="dashboard.current.loader.load_gist">
+        </div>
+        <div class="editor-option">
+          <label class="small">Elasticsearch</label><input type="checkbox" ng-model="dashboard.current.loader.load_elasticsearch" ng-checked="dashboard.current.loader.load_elasticsearch">
+        </div>
+        <div class="editor-option" ng-show="dashboard.current.loader.load.elasticsearch">
+          <label class="small">ES list size</label><input class="input-mini" type="number" ng-model="dashboard.current.loader.load_elasticsearch_size">
+        </div>
+      </div>
+      <div class="section">
+      <h5>Sharing</h5>
+        <div class="editor-option" >
+          <label class="small">Allow Sharing <tip>Allow generating adhoc links to dashboards</tip></label><input type="checkbox" ng-model="dashboard.current.loader.save_temp" ng-checked="dashboard.current.loader.save_temp">
+        </div>
+        <div class="editor-option" ng-show="dashboard.current.loader.save_temp">
+          <label class="small">TTL <tip>Expire temp urls</tip></label><input type="checkbox" ng-model="dashboard.current.loader.save_temp_ttl_enable">
+        </div>
+        <div class="editor-option" ng-show="dashboard.current.loader.save_temp &amp;&amp; dashboard.current.loader.save_temp_ttl_enable">
+          <label class="small">TTL Duration <tip>Elasticsearch date math, eg: 1m,1d,1w,30d  </tip></label><input class="input-small" type="text" ng-model="dashboard.current.loader.save_temp_ttl">
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div ng-if="editor.index == 3">
+    <div class="editor-row">
+      <div class="section">
+        <h5>Pulldowns</h5>
+        <div class="editor-option" ng-repeat="pulldown in dashboard.current.pulldowns">
+          <label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
+        </div>
+        <div class="editor-option" ng-repeat="pulldown in dashboard.current.nav|editable">
+          <label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div ng-repeat="pulldown in dashboard.current.nav|editable" ng-controller="PulldownCtrl" ng-show="editor.index == 4+$index">
+    <ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
+    <button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
+  </div>
+
+
+</div>
+
+<div class="modal-footer">
+  <button type="button" ng-click="add_row(dashboard.current,row); reset_row();" class="btn btn-info" ng-show="editor.index == 2">Create Row</button>
+  <button type="button" class="btn btn-success" ng-click="editor.index=0;editSave(dashboard);dismiss();reset_panel();dashboard.refresh()">Save</button>
+  <button type="button" class="btn btn-danger" ng-click="editor.index=0;dismiss();reset_panel();dashboard.refresh()">Cancel</button>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/inspector.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/inspector.html b/metron-ui/lib/public/app/partials/inspector.html
new file mode 100755
index 0000000..0b2ebe6
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/inspector.html
@@ -0,0 +1,15 @@
+<div class="modal-header">
+  <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+  <h3>Last Elasticsearch Query</h3>
+</div>
+<div class="modal-body">
+
+  <div>
+    <pre>curl -XGET '{{config.elasticsearch}}/{{dashboard.indices|stringify}}/_search?pretty' -d '{{inspector}}'
+    </pre>
+  </div>
+  
+</div>
+<div class="modal-footer">
+  <button type="button" class="btn btn-success" ng-click="dismiss()">Close</button>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/load.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/load.html b/metron-ui/lib/public/app/partials/load.html
new file mode 100755
index 0000000..27d012b
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/load.html
@@ -0,0 +1,4 @@
+<div style="margin-top:50px" ng-controller="dashcontrol">
+    <strong>type: </strong>{{type}} <br>
+    <strong>id: </strong>{{id}} <br>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/modal.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/modal.html b/metron-ui/lib/public/app/partials/modal.html
new file mode 100755
index 0000000..38d86cd
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/modal.html
@@ -0,0 +1,12 @@
+<div class="modal-header">
+  <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+  <h3>{{modal.title}}</h3>
+</div>
+<div class="modal-body">
+
+  <div ng-bind-html='modal.body'></div>
+
+</div>
+<div class="modal-footer">
+  <button type="button" class="btn btn-danger" ng-click="dismiss()">Close</button>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/paneladd.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/paneladd.html b/metron-ui/lib/public/app/partials/paneladd.html
new file mode 100755
index 0000000..5b780fb
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/paneladd.html
@@ -0,0 +1,6 @@
+  <div ng-include="'app/partials/panelgeneral.html'"></div>
+  <div ng-include="edit_path(panel.type)"></div>
+  <div ng-repeat="tab in panelMeta.editorTabs">
+    <h5>{{tab.title}}</h5>
+    <div ng-include="tab.src"></div>
+  </div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/paneleditor.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/paneleditor.html b/metron-ui/lib/public/app/partials/paneleditor.html
new file mode 100755
index 0000000..d66070b
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/paneleditor.html
@@ -0,0 +1,24 @@
+<div bindonce class="modal-body">
+  <div class="pull-right editor-title" bo-text="panel.type+' settings'"></div>
+  <div ng-model="editor.index" bs-tabs>
+    <div ng-repeat="tab in setEditorTabs(panelMeta)" data-title="{{tab}}">
+    </div>
+  </div>
+  <div ng-show="editorTabs[editor.index] == 'General'">
+    <div ng-include src="'app/partials/panelgeneral.html'"></div>
+  </div>
+
+  <div ng-show="editorTabs[editor.index] == 'Panel'">
+    <div ng-include src="edit_path(panel.type)"></div>
+  </div>
+
+  <div ng-repeat="tab in panelMeta.editorTabs" ng-show="editorTabs[editor.index] == tab.title">
+    <div ng-include src="tab.src"></div>
+  </div>
+</div>
+
+<div class="modal-footer">
+  <!-- close_edit() is provided here to allow for a scope to perform action on dismiss -->
+  <button type="button" class="btn btn-success" ng-click="editor.index=0;editSave(panel);close_edit();dismiss()">Save</button>
+  <button type="button" class="btn btn-danger" ng-click="editor.index=0;dismiss()">Cancel</button>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/panelgeneral.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/panelgeneral.html b/metron-ui/lib/public/app/partials/panelgeneral.html
new file mode 100755
index 0000000..22e7494
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/panelgeneral.html
@@ -0,0 +1,24 @@
+  <div class="editor-row">
+    <div class="section">
+      <strong>{{panelMeta.status}}</strong> // <span ng-bind-html="panelMeta.description"></span>
+    </div>
+  </div>
+  <div class="editor-row">
+    <div class="section">
+      <div class="editor-option">
+        <label class="small">Title</label><input type="text" class="input-medium" ng-model='panel.title'></input>
+      </div>
+      <div class="editor-option" ng-hide="panel.sizeable == false">
+        <label class="small">Span</label> <select class="input-mini" ng-model="panel.span" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10,11,12]"></select>
+      </div>
+      <div class="editor-option">
+        <label class="small">Editable</label><input type="checkbox" ng-model="panel.editable" ng-checked="panel.editable">
+      </div>
+      <div class="editor-option" ng-show="!_.isUndefined(panel.spyable)">
+        <label class="small">
+          Inspect <i class="icon-question-sign" bs-tooltip="'Allow query reveal via <i class=icon-info-sign></i>'"></i>
+        </label>
+        <input type="checkbox" ng-model="panel.spyable" ng-checked="panel.spyable">
+      </div>
+    </div>
+  </div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/querySelect.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/querySelect.html b/metron-ui/lib/public/app/partials/querySelect.html
new file mode 100755
index 0000000..a580a48
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/querySelect.html
@@ -0,0 +1,28 @@
+  <div class="row-fluid">
+    <style>
+      .querySelect .query {
+        margin-right: 5px;
+      }
+      .querySelect .selected {
+        border: 3px solid;
+      }
+      .querySelect .unselected {
+        border: 0px solid;
+      }
+    </style>
+    <div class="span2" style="margin-left:0px">
+      <label class="small">Queries</label>
+      <select class="input-small" ng-change="set_refresh(true);" ng-model="panel.queries.mode" ng-options="f for f in ['all','pinned','unpinned','selected']"></select>
+    </div>
+    <div class="span2">
+      <label class="small">Locked</label>
+      <input type="checkbox" ng-change="set_refresh(true);" ng-model="panel.locked">
+    </div>
+    <div class="span7 querySelect" ng-show="panel.queries.mode == 'selected'">
+      <label class="small">Selected Queries</label>
+      <span ng-style="{'border-color': querySrv.list()[id].color}" ng-class="{selected:_.contains(panel.queries.ids,id),unselected:!_.contains(panel.queries.ids,id)}" ng-repeat="id in querySrv.ids()" ng-click="panel.queries.ids = _.toggleInOut(panel.queries.ids,id);set_refresh(true);" class="query pointer badge">
+        <i class="icon-circle" ng-style="{color: querySrv.list()[id].color}"></i>
+        <span> {{querySrv.list()[id].alias || querySrv.list()[id].query}}</span>
+      </span>
+    </div>
+  </div>

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/partials/roweditor.html
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/partials/roweditor.html b/metron-ui/lib/public/app/partials/roweditor.html
new file mode 100755
index 0000000..b2240e7
--- /dev/null
+++ b/metron-ui/lib/public/app/partials/roweditor.html
@@ -0,0 +1,67 @@
+<div class="modal-body">
+  <div class="pull-right editor-title">Row settings</div>
+
+  <div ng-model="editor.index" bs-tabs>
+    <div ng-repeat="tab in ['General','Panels','Add Panel']" data-title="{{tab}}">
+    </div>
+  </div>
+
+  <div class="editor-row" ng-if="editor.index == 0">
+    <div class="editor-option">
+      <label class="small">Title</label><input type="text" class="input-medium" ng-model='row.title'></input>
+    </div>
+    <div class="editor-option">
+      <label class="small">Height</label><input type="text" class="input-mini" ng-model='row.height'></input>
+    </div>
+    <div class="editor-option">
+      <label class="small"> Editable </label><input type="checkbox" ng-model="row.editable" ng-checked="row.editable" />
+    </div>
+    <div class="editor-option">
+      <label class="small"> Collapsable </label><input type="checkbox" ng-model="row.collapsable" ng-checked="row.collapsable" />
+    </div>
+  </div>
+  <div class="row-fluid" ng-if="editor.index == 1">
+    <div class="span12">
+      <h4>Panels</h4>
+      <table class="table table-condensed table-striped">
+        <thead>
+          <th>Title</th>
+          <th>Type</th>
+          <th>Span <span class="small">({{rowSpan(row)}}/12)</span></th>
+          <th>Delete</th>
+          <th>Move</th>
+          <th></th>
+          <th>Hide</th>
+        </thead>
+        <tr ng-repeat="panel in row.panels">
+          <td>{{panel.title}}</td>
+          <td>{{panel.type}}</td>
+          <td><select ng-hide="panel.sizeable == false" class="input-mini" ng-model="panel.span" ng-options="size for size in [1,2,3,4,5,6,7,8,9,10,11,12]"></select></td>
+          <td><i ng-click="row.panels = _.without(row.panels,panel)" class="pointer icon-remove"></i></td>
+          <td><i ng-click="_.move(row.panels,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
+          <td><i ng-click="_.move(row.panels,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
+          <td><input type="checkbox" ng-model="panel.hide" ng-checked="panel.hide"></td>
+        </tr>
+      </table>
+    </div>
+  </div>
+  <div class="row-fluid" ng-if="editor.index == 2">
+    <h4>Select Panel Type</h4>
+    <form class="form-inline">
+      <select class="input-medium" ng-model="panel.type" ng-options="panelType for panelType in dashboard.availablePanels|stringSort"></select>
+      <small ng-show="rowSpan(row) > 11">
+        Note: This row is full, new panels will wrap to a new line. You should add another row.
+      </small>
+    </form>
+
+    <div ng-show="!(_.isUndefined(panel.type))">
+      <div add-panel="{{panel.type}}"></div>
+    </div>
+  </div>
+</div>
+<div class="modal-footer">
+  <button ng-show="editor.index == 1" ng-click="editor.index = 2;" class="btn btn-info" ng-disabled="panel.loadingEditor">Add Panel</button>
+  <button ng-show="panel.type && editor.index == 2" ng-click="editSave(row);add_panel(row,panel);reset_panel();editor.index = 0;dismiss();" class="btn btn-success" ng-disabled="panel.loadingEditor">Save</button>
+  <button ng-hide="panel.type && editor.index == 2" ng-click="editor.index=0;editSave(row);dismiss();reset_panel();close_edit()" class="btn btn-success">Save</button>
+  <button type="button" class="btn btn-danger" ng-click="editor.index=0;dismiss();reset_panel();close_edit()">Cancel</button>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/alertSrv.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/alertSrv.js b/metron-ui/lib/public/app/services/alertSrv.js
new file mode 100755
index 0000000..8d3d095
--- /dev/null
+++ b/metron-ui/lib/public/app/services/alertSrv.js
@@ -0,0 +1,49 @@
+define([
+  'angular',
+  'lodash'
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('alertSrv', function($timeout) {
+    var self = this;
+
+    // List of all alert objects
+    this.list = [];
+
+    this.set = function(title,text,severity,timeout) {
+      var
+        _a = {
+          title: title || '',
+          text: text || '',
+          severity: severity || 'info',
+        },
+        _ca = angular.toJson(_a),
+        _clist = _.map(self.list,function(alert){return angular.toJson(alert);});
+
+      // If we already have this alert, remove it and add a new one
+      // Why do this instead of skipping the add because it resets the timer
+      if(_.contains(_clist,_ca)) {
+        _.remove(self.list,_.indexOf(_clist,_ca));
+      }
+
+      self.list.push(_a);
+      if (timeout > 0) {
+        $timeout(function() {
+          self.list = _.without(self.list,_a);
+        }, timeout);
+      }
+      return(_a);
+    };
+
+    this.clear = function(alert) {
+      self.list = _.without(self.list,alert);
+    };
+
+    this.clearAll = function() {
+      self.list = [];
+    };
+  });
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/all.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/all.js b/metron-ui/lib/public/app/services/all.js
new file mode 100755
index 0000000..fdc5408
--- /dev/null
+++ b/metron-ui/lib/public/app/services/all.js
@@ -0,0 +1,12 @@
+define([
+  './alertSrv',
+  './dashboard',
+  './fields',
+  './filterSrv',
+  './kbnIndex',
+  './querySrv',
+  './timer',
+  './panelMove',
+  './esVersion'
+],
+function () {});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/dashboard.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/dashboard.js b/metron-ui/lib/public/app/services/dashboard.js
new file mode 100755
index 0000000..6b4634c
--- /dev/null
+++ b/metron-ui/lib/public/app/services/dashboard.js
@@ -0,0 +1,529 @@
+define([
+  'angular',
+  'jquery',
+  'kbn',
+  'lodash',
+  'config',
+  'moment',
+  'modernizr',
+  'filesaver',
+  'blob'
+],
+function (angular, $, kbn, _, config, moment, Modernizr) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('dashboard', function(
+    $routeParams, $http, $rootScope, $injector, $location, $timeout,
+    ejsResource, timer, kbnIndex, alertSrv, esVersion, esMinVersion
+  ) {
+    // A hash of defaults to use when loading a dashboard
+
+    var _dash = {
+      title: "",
+      style: "dark",
+      editable: true,
+      realtime: true, // defaults to false
+      failover: false,
+      panel_hints: true,
+      rows: [],
+      pulldowns: [
+        {
+          type: 'query',
+        },
+        {
+          type: 'filtering'
+        }
+      ],
+      nav: [
+        {
+          type: 'timepicker'
+        }
+      ],
+      services: {},
+      loader: {
+        save_gist: false,
+        save_elasticsearch: true,
+        save_local: true,
+        save_default: true,
+        save_temp: true,
+        save_temp_ttl_enable: true,
+        save_temp_ttl: '30d',
+        load_gist: false,
+        load_elasticsearch: true,
+        load_elasticsearch_size: 20,
+        load_local: false,
+        hide: false
+      },
+      index: {
+        interval: 'none',
+        pattern: '_all',
+        default: 'INDEX_MISSING',
+        warm_fields: true
+      },
+      refresh: false
+    };
+
+    // An elasticJS client to use
+    var ejs = ejsResource(config.elasticsearch);
+
+    var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
+
+    // Store a reference to this
+    var self = this;
+    var filterSrv,querySrv;
+
+    this.current = _.clone(_dash);
+    this.last = {};
+    this.availablePanels = [];
+
+    $rootScope.$on('$routeChangeSuccess',function(){
+      // Clear the current dashboard to prevent reloading
+      self.current = {};
+      self.indices = [];
+      esVersion.isMinimum().then(function(isMinimum) {
+        if(isMinimum) {
+          route();
+        } else {
+          alertSrv.set('Upgrade Required',"Your version of Elasticsearch is too old. Kibana requires" +
+            " Elasticsearch " + esMinVersion + " or above.", "error");
+        }
+      });
+    });
+
+    var route = function() {
+      // Is there a dashboard type and id in the URL?
+      if(!(_.isUndefined($routeParams.kbnType)) && !(_.isUndefined($routeParams.kbnId))) {
+        var _type = $routeParams.kbnType;
+        var _id = $routeParams.kbnId;
+
+        switch(_type) {
+        case ('elasticsearch'):
+          self.elasticsearch_load('dashboard',_id);
+          break;
+        case ('temp'):
+          self.elasticsearch_load('temp',_id);
+          break;
+        case ('file'):
+          self.file_load(_id);
+          break;
+        case('script'):
+          self.script_load(_id);
+          break;
+        case('local'):
+          self.local_load();
+          break;
+        default:
+          $location.path(config.default_route);
+        }
+      // No dashboard in the URL
+      } else {
+        // Check if browser supports localstorage, and if there's an old dashboard. If there is,
+        // inform the user that they should save their dashboard to Elasticsearch and then set that
+        // as their default
+        if (Modernizr.localstorage) {
+          if(!(_.isUndefined(window.localStorage['dashboard'])) && window.localStorage['dashboard'] !== '') {
+            $location.path(config.default_route);
+            alertSrv.set('Saving to browser storage has been replaced',' with saving to Elasticsearch.'+
+              ' Click <a href="#/dashboard/local/deprecated">here</a> to load your old dashboard anyway.');
+          } else if(!(_.isUndefined(window.localStorage.kibanaDashboardDefault))) {
+            $location.path(window.localStorage.kibanaDashboardDefault);
+          } else {
+            $location.path(config.default_route);
+          }
+        // No? Ok, grab the default route, its all we have now
+        } else {
+          $location.path(config.default_route);
+        }
+      }
+    };
+
+    // Since the dashboard is responsible for index computation, we can compute and assign the indices
+    // here before telling the panels to refresh
+    this.refresh = function() {
+      if(self.current.index.interval !== 'none') {
+        if(_.isUndefined(filterSrv)) {
+          return;
+        }
+        if(filterSrv.idsByType('time').length > 0) {
+          var _range = filterSrv.timeRange('last');
+          kbnIndex.indices(_range.from,_range.to,
+            self.current.index.pattern,self.current.index.interval
+          ).then(function (p) {
+            if(p.length > 0) {
+              self.indices = p;
+            } else {
+              // Option to not failover
+              if(self.current.failover) {
+                self.indices = [self.current.index.default];
+              } else {
+                // Do not issue refresh if no indices match. This should be removed when panels
+                // properly understand when no indices are present
+                alertSrv.set('No results','There were no results because no indices were found that match your'+
+                  ' selected time span','info',5000);
+                return false;
+              }
+            }
+            // Don't resolve queries until indices are updated
+            querySrv.resolve().then(function(){$rootScope.$broadcast('refresh');});
+          });
+        } else {
+          if(self.current.failover) {
+            self.indices = [self.current.index.default];
+            querySrv.resolve().then(function(){$rootScope.$broadcast('refresh');});
+          } else {
+            alertSrv.set("No time filter",
+              'Timestamped indices are configured without a failover. Waiting for time filter.',
+              'info',5000);
+          }
+        }
+      } else {
+        self.indices = [self.current.index.default];
+        querySrv.resolve().then(function(){$rootScope.$broadcast('refresh');});
+      }
+    };
+
+    var dash_defaults = function(dashboard) {
+      _.defaults(dashboard,_dash);
+      _.defaults(dashboard.index,_dash.index);
+      _.defaults(dashboard.loader,_dash.loader);
+      return _.cloneDeep(dashboard);
+    };
+
+    this.dash_load = function(dashboard) {
+      // Cancel all timers
+      timer.cancel_all();
+
+      // Make sure the dashboard being loaded has everything required
+      dashboard = dash_defaults(dashboard);
+
+      // If not using time based indices, use the default index
+      if(dashboard.index.interval === 'none') {
+        self.indices = [dashboard.index.default];
+      }
+
+      // Set the current dashboard
+      self.current = _.clone(dashboard);
+
+      // Delay this until we're sure that querySrv and filterSrv are ready
+      $timeout(function() {
+        // Ok, now that we've setup the current dashboard, we can inject our services
+        if(!_.isUndefined(self.current.services.query)) {
+          querySrv = $injector.get('querySrv');
+          querySrv.init();
+        }
+        if(!_.isUndefined(self.current.services.filter)) {
+          filterSrv = $injector.get('filterSrv');
+          filterSrv.init();
+        }
+      },0).then(function() {
+        // Call refresh to calculate the indices and notify the panels that we're ready to roll
+        self.refresh();
+      });
+
+      if(dashboard.refresh) {
+        self.set_interval(dashboard.refresh);
+      }
+
+      // Set the available panels for the "Add Panel" drop down
+      self.availablePanels = _.difference(config.panel_names,
+        _.pluck(_.union(self.current.nav,self.current.pulldowns),'type'));
+
+      // Take out any that we're not allowed to add from the gui.
+      self.availablePanels = _.difference(self.availablePanels,config.hidden_panels);
+
+      return true;
+    };
+
+    this.gist_id = function(string) {
+      if(self.is_gist(string)) {
+        return string.match(gist_pattern)[0].replace(/.*\//, '');
+      }
+    };
+
+    this.is_gist = function(string) {
+      if(!_.isUndefined(string) && string !== '' && !_.isNull(string.match(gist_pattern))) {
+        return string.match(gist_pattern).length > 0 ? true : false;
+      } else {
+        return false;
+      }
+    };
+
+    this.to_file = function() {
+      var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
+      // from filesaver.js
+      window.saveAs(blob, self.current.title+"-"+new Date().getTime());
+      return true;
+    };
+
+    this.set_default = function(route) {
+      if (Modernizr.localstorage) {
+        // Purge any old dashboards
+        if(!_.isUndefined(window.localStorage['dashboard'])) {
+          delete window.localStorage['dashboard'];
+        }
+        window.localStorage.kibanaDashboardDefault = route;
+        return true;
+      } else {
+        return false;
+      }
+    };
+
+    this.purge_default = function() {
+      if (Modernizr.localstorage) {
+        // Purge any old dashboards
+        if(!_.isUndefined(window.localStorage['dashboard'])) {
+
+          delete window.localStorage['dashboard'];
+        }
+        delete window.localStorage.kibanaDashboardDefault;
+        return true;
+      } else {
+        return false;
+      }
+    };
+
+    // TOFIX: Pretty sure this breaks when you're on a saved dashboard already
+    this.share_link = function(title,type,id) {
+      return {
+        location  : window.location.href.replace(window.location.hash,""),
+        type      : type,
+        id        : id,
+        link      : window.location.href.replace(window.location.hash,"")+"#dashboard/"+type+"/"+encodeURIComponent(id),
+        title     : title
+      };
+    };
+
+    var renderTemplate = function(json,params) {
+      var _r;
+      _.templateSettings = {interpolate : /\{\{(.+?)\}\}/g};
+      var template = _.template(json);
+      var rendered = template({ARGS:params});
+      try {
+        _r = angular.fromJson(rendered);
+      } catch(e) {
+        _r = false;
+      }
+      return _r;
+    };
+
+    this.local_load = function() {
+      var dashboard = JSON.parse(window.localStorage['dashboard']);
+      dashboard.rows.unshift({
+        height: "30",
+        title: "Deprecation Notice",
+        panels: [
+          {
+            title: 'WARNING: Legacy dashboard',
+            type: 'text',
+            span: 12,
+            mode: 'html',
+            content: 'This dashboard has been loaded from the browsers local cache. If you use '+
+            'another brower or computer you will not be able to access it! '+
+            '\n\n  <h4>Good news!</h4> Kibana'+
+            ' now stores saved dashboards in Elasticsearch. Click the <i class="icon-save"></i> '+
+            'button in the top left to save this dashboard. Then select "Set as Home" from'+
+            ' the "advanced" sub menu to automatically use the stored dashboard as your Kibana '+
+            'landing page afterwards'+
+            '<br><br><strong>Tip:</strong> You may with to remove this row before saving!'
+          }
+        ]
+      });
+      self.dash_load(dashboard);
+    };
+
+    this.file_load = function(file) {
+      return $http({
+        url: "app/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(),
+        method: "GET",
+        transformResponse: function(response) {
+          return renderTemplate(response,$routeParams);
+        }
+      }).then(function(result) {
+        if(!result) {
+          return false;
+        }
+        self.dash_load(dash_defaults(result.data));
+        return true;
+      },function() {
+        alertSrv.set('Error',"Could not load <i>dashboards/"+file+"</i>. Please make sure it exists" ,'error');
+        return false;
+      });
+    };
+
+    this.elasticsearch_load = function(type,id) {
+      var successcb = function(data) {
+        var response = renderTemplate(angular.fromJson(data)._source.dashboard, $routeParams);
+        self.dash_load(response);
+      };
+      var errorcb = function(data, status) {
+        if(status === 0) {
+          alertSrv.set('Error',"Could not contact Elasticsearch at "+ejs.config.server+
+            ". Please ensure that Elasticsearch is reachable from your system." ,'error');
+        } else {
+          alertSrv.set('Error',"Could not find "+id+". If you"+
+            " are using a proxy, ensure it is configured correctly",'error');
+        }
+        return false;
+      };
+
+      ejs.client.get(
+        "/" + config.kibana_index + "/"+type+"/"+id+'?' + new Date().getTime(),
+        null, successcb, errorcb);
+
+    };
+
+    this.script_load = function(file) {
+      return $http({
+        url: "app/dashboards/"+file.replace(/\.(?!js)/,"/"),
+        method: "GET",
+        transformResponse: function(response) {
+          /*jshint -W054 */
+          var _f = new Function('ARGS','kbn','_','moment','window','document','angular','require','define','$','jQuery',response);
+          return _f($routeParams,kbn,_,moment);
+        }
+      }).then(function(result) {
+        if(!result) {
+          return false;
+        }
+        self.dash_load(dash_defaults(result.data));
+        return true;
+      },function() {
+        alertSrv.set('Error',
+          "Could not load <i>scripts/"+file+"</i>. Please make sure it exists and returns a valid dashboard" ,
+          'error');
+        return false;
+      });
+    };
+
+    this.elasticsearch_save = function(type,title,ttl) {
+      // Clone object so we can modify it without influencing the existing obejct
+      var save = _.clone(self.current);
+      var id;
+
+      // Change title on object clone
+      if (type === 'dashboard') {
+        id = save.title = _.isUndefined(title) ? self.current.title : title;
+      }
+
+      // Create request with id as title. Rethink this.
+      var request = ejs.Document(config.kibana_index,type,id).source({
+        user: 'guest',
+        group: 'guest',
+        title: save.title,
+        dashboard: angular.toJson(save)
+      });
+
+      request = type === 'temp' && ttl ? request.ttl(ttl) : request;
+
+      return request.doIndex(
+        // Success
+        function(result) {
+          if(type === 'dashboard') {
+            $location.path('/dashboard/elasticsearch/'+title);
+          }
+          return result;
+        },
+        // Failure
+        function() {
+          return false;
+        }
+      );
+    };
+
+    this.elasticsearch_delete = function(id) {
+      return ejs.Document(config.kibana_index,'dashboard',id).doDelete(
+        // Success
+        function(result) {
+          return result;
+        },
+        // Failure
+        function() {
+          return false;
+        }
+      );
+    };
+
+    this.elasticsearch_list = function(query,count) {
+      var request = ejs.Request().indices(config.kibana_index).types('dashboard');
+      return request.query(
+        ejs.QueryStringQuery(query || '*')
+        ).size(count).doSearch(
+          // Success
+          function(result) {
+            return result;
+          },
+          // Failure
+          function() {
+            return false;
+          }
+        );
+    };
+
+    this.save_gist = function(title,dashboard) {
+      var save = _.clone(dashboard || self.current);
+      save.title = title || self.current.title;
+      return $http({
+        url: "https://api.github.com/gists",
+        method: "POST",
+        data: {
+          "description": save.title,
+          "public": false,
+          "files": {
+            "kibana-dashboard.json": {
+              "content": angular.toJson(save,true)
+            }
+          }
+        }
+      }).then(function(data) {
+        return data.data.html_url;
+      }, function() {
+        return false;
+      });
+    };
+
+    this.gist_list = function(id) {
+      return $http.jsonp("https://api.github.com/gists/"+id+"?callback=JSON_CALLBACK"
+      ).then(function(response) {
+        var files = [];
+        _.each(response.data.data.files,function(v) {
+          try {
+            var file = JSON.parse(v.content);
+            files.push(file);
+          } catch(e) {
+            return false;
+          }
+        });
+        return files;
+      }, function() {
+        return false;
+      });
+    };
+
+    this.start_scheduled_refresh = function (after_ms) {
+      timer.cancel(self.refresh_timer);
+      self.refresh_timer = timer.register($timeout(function () {
+        self.start_scheduled_refresh(after_ms);
+        self.refresh();
+      }, after_ms));
+    };
+
+    this.cancel_scheduled_refresh = function () {
+      timer.cancel(self.refresh_timer);
+    };
+
+    this.set_interval = function (interval) {
+      self.current.refresh = interval;
+      if (interval) {
+        var _i = kbn.interval_to_ms(interval);
+        this.start_scheduled_refresh(_i);
+      } else {
+        this.cancel_scheduled_refresh();
+      }
+    };
+
+
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/esVersion.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/esVersion.js b/metron-ui/lib/public/app/services/esVersion.js
new file mode 100755
index 0000000..1a31e85
--- /dev/null
+++ b/metron-ui/lib/public/app/services/esVersion.js
@@ -0,0 +1,195 @@
+define([
+  'angular',
+  'lodash',
+  'config'
+],
+function (angular, _, config) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('esVersion', function($http, alertSrv, esMinVersion, $q, ejsResource) {
+
+    this.versions = [];
+
+    var ejs = ejsResource(config.elasticsearch);
+
+
+    // save a reference to this
+    var self = this,
+      defer = $q.defer();
+
+    this.init = function() {
+      getVersions();
+    };
+
+    var getVersions = function() {
+      if(self.versions.length !== 0) {
+        defer.resolve(self.versions);
+        return defer.promise;
+      } else {
+        var nodeInfo = ejs.client.get('/_nodes',
+          undefined, undefined, function(data, status) {
+          if(status === 0) {
+            alertSrv.set('Error',"Could not contact Elasticsearch at "+ejs.client.server()+
+              ". Please ensure that Elasticsearch is reachable from your system." ,'error');
+          } else {
+            alertSrv.set('Error',"Could not reach "+ejs.client.server()+"/_nodes. If you"+
+            " are using a proxy, ensure it is configured correctly",'error');
+          }
+          return;
+        });
+
+        return nodeInfo.then(function(p) {
+          _.each(p.nodes, function(v) {
+            self.versions.push(v.version.split('-')[0]);
+          });
+          self.versions = sortVersions(_.uniq(self.versions));
+          return self.versions;
+        });
+      }
+
+    };
+
+    // Get the max version in this cluster
+    this.max = function(versions) {
+      return _.last(versions);
+    };
+
+    // Return the lowest version in the cluster
+    this.min = function(versions) {
+      return _.first(versions);
+    };
+
+    // Sort versions from lowest to highest
+    var sortVersions = function(versions) {
+      var _versions = _.clone(versions),
+        _r = [];
+
+      while(_r.length < versions.length) {
+        var _h = "0";
+        /*jshint -W083 */
+        _.each(_versions,function(v){
+          if(self.compare(_h,v)) {
+            _h = v;
+          }
+        });
+        _versions = _.without(_versions,_h);
+        _r.push(_h);
+      }
+      return _r.reverse();
+    };
+
+    /*
+      Takes a version string with one of the following optional comparison prefixes: >,>=,<.<=
+      and evaluates if the cluster meets the requirement. If the prefix is omitted exact match
+      is assumed
+    */
+    this.is = function(equation) {
+      var _v = equation,
+        _cf;
+
+      if(_v.charAt(0) === '>') {
+        _cf = _v.charAt(1) === '=' ? self.gte(_v.slice(2)) : self.gt(_v.slice(1));
+      } else if (_v.charAt(0) === '<') {
+        _cf = _v.charAt(1) === '=' ? self.lte(_v.slice(2)) : self.lt(_v.slice(1));
+      } else {
+        _cf = self.eq(_v);
+      }
+
+      return _cf;
+    };
+
+    this.isMinimum = function() {
+      return self.gte(esMinVersion);
+    };
+
+    // check if lowest version in cluster = `version`
+    this.eq = function(version) {
+      return getVersions().then(function(v) {
+        return version === self.min(v) ? true : false;
+      });
+
+    };
+
+    // version > lowest version in cluster?
+    this.gt = function(version) {
+      return getVersions().then(function(v) {
+        return version === self.min(v) ? false : self.gte(version);
+      });
+
+    };
+
+    // version < highest version in cluster?
+    this.lt = function(version) {
+      return getVersions().then(function(v) {
+        return version === self.max(v) ? false : self.lte(version);
+      });
+
+    };
+
+    // Check if the lowest version in the cluster is >= to `version`
+    this.gte = function(version) {
+      return getVersions().then(function(v) {
+        return self.compare(version,self.min(v));
+      });
+
+    };
+
+    // Check if the highest version in the cluster is <= to `version`
+    this.lte = function(version) {
+      return getVersions().then(function(v) {
+        return self.compare(self.max(v),version);
+      });
+    };
+
+    // Determine if a specific version is greater than or equal to another
+    this.compare = function (required,installed) {
+      if(!required || !installed) {
+        return undefined;
+      }
+
+      var a = installed.split('.');
+      var b = required.split('.');
+      var i;
+
+      // leave suffixes as is ("RC1 or -SNAPSHOT")
+      for (i = 0; i < Math.min(a.length, 3); ++i) {
+        a[i] = Number(a[i]);
+      }
+      for (i = 0; i < Math.min(b.length, 3); ++i) {
+        b[i] = Number(b[i]);
+      }
+      if (a.length === 2) {
+        a[2] = 0;
+      }
+
+      if (a[0] > b[0]){return true;}
+      if (a[0] < b[0]){return false;}
+
+      if (a[1] > b[1]){return true;}
+      if (a[1] < b[1]){return false;}
+
+      if (a[2] > b[2]){return true;}
+      if (a[2] < b[2]){return false;}
+
+      if (a.length > 3) {
+        // rc/beta suffix
+        if (b.length <= 3) {
+          return false;
+        } // no suffix on b -> a<b
+        return a[3] >= b[3];
+      }
+      if (b.length > 3) {
+        // b has a suffix but a not -> a>b
+        return true;
+      }
+
+      return true;
+    };
+
+    this.init();
+
+  });
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/fields.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/fields.js b/metron-ui/lib/public/app/services/fields.js
new file mode 100755
index 0000000..3d01490
--- /dev/null
+++ b/metron-ui/lib/public/app/services/fields.js
@@ -0,0 +1,111 @@
+define([
+  'angular',
+  'lodash',
+  'config'
+],
+function (angular, _, config) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('fields', function(dashboard, $rootScope, $http, esVersion, alertSrv, ejsResource) {
+
+    // Save a reference to this
+    var self = this;
+
+    var ejs = ejsResource(config.elasticsearch);
+
+
+    this.list = ['_type'];
+    this.indices = [];
+
+    // Stop tracking the full mapping, too expensive, instead we only remember the index names
+    // we've already seen.
+    //
+    $rootScope.$watch(function(){return dashboard.indices;},function(n) {
+      if(!_.isUndefined(n) && n.length && dashboard.current.index.warm_fields) {
+        // Only get the mapping for indices we don't know it for
+        var indices = _.difference(n,_.keys(self.indices));
+        // Only get the mapping if there are new indices
+        if(indices.length > 0) {
+          self.map(indices).then(function(result) {
+            self.indices = _.union(self.indices,_.keys(result));
+            self.list = mapFields(result);
+          });
+        }
+      }
+    });
+
+    var mapFields = function (m) {
+      var fields = [];
+      _.each(m, function(types) {
+        _.each(types, function(type) {
+          fields = _.difference(_.union(fields,_.keys(type)),
+            ['_parent','_routing','_size','_ttl','_all','_uid','_version','_boost','_source']);
+        });
+      });
+      return fields;
+    };
+
+    this.map = function(indices) {
+      var request = ejs.client.get('/' + indices.join(',') + "/_mapping",
+        undefined, undefined, function(data, status) {
+          if(status === 0) {
+            alertSrv.set('Error',"Could not contact Elasticsearch at "+ejs.config.server+
+              ". Please ensure that Elasticsearch is reachable from your system." ,'error');
+          } else {
+            alertSrv.set('Error',"No index found at "+ejs.config.server+"/" +
+              indices.join(',')+"/_mapping. Please create at least one index."  +
+              "If you're using a proxy ensure it is configured correctly.",'error');
+          }
+        });
+
+      // Flatten the mapping of each index into dot notated keys.
+      return request.then(function(p) {
+        var mapping = {};
+        return esVersion.gte('1.0.0.RC1').then(function(version) {
+          _.each(p, function(indexMap,index) {
+            mapping[index] = {};
+            _.each((version ? indexMap.mappings : indexMap), function (typeMap,type) {
+              mapping[index][type] = flatten(typeMap);
+            });
+          });
+          return mapping;
+        });
+      });
+    };
+
+    // This should understand both the 1.0 format and the 0.90 format for mappings. Ugly.
+    var flatten = function(obj,prefix) {
+      var propName = (prefix) ? prefix :  '',
+        dot = (prefix) ? '.':'',
+        ret = {};
+      for(var attr in obj){
+        if(attr === 'dynamic_templates' || attr === '_default_') {
+          continue;
+        }
+        // For now only support multi field on the top level
+        // and if there is a default field set.
+        if(obj[attr]['type'] === 'multi_field') {
+          ret[attr] = obj[attr]['fields'][attr] || obj[attr];
+          var keys = _.without(_.keys(obj[attr]['fields']),attr);
+          for(var key in keys) {
+            ret[attr+'.'+keys[key]] = obj[attr]['fields'][keys[key]];
+          }
+        } else if (attr === 'properties' || attr ==='fields') {
+          _.extend(ret,flatten(obj[attr], propName));
+        } else if(typeof obj[attr] === 'object' &&
+          (!_.isUndefined(obj[attr].type) || !_.isUndefined(obj[attr].properties))){
+          _.extend(ret,flatten(obj[attr], propName + dot + attr));
+        } else {
+          if(propName !== '') {
+            ret[propName] = obj;
+          }
+        }
+      }
+      return ret;
+    };
+
+  });
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/filterSrv.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/filterSrv.js b/metron-ui/lib/public/app/services/filterSrv.js
new file mode 100755
index 0000000..7201946
--- /dev/null
+++ b/metron-ui/lib/public/app/services/filterSrv.js
@@ -0,0 +1,249 @@
+define([
+  'angular',
+  'lodash',
+  'config',
+  'kbn'
+], function (angular, _, config, kbn) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('filterSrv', function(dashboard, ejsResource, $rootScope, $timeout) {
+    // Create an object to hold our service state on the dashboard
+    dashboard.current.services.filter = dashboard.current.services.filter || {};
+
+    // Defaults for it
+    var _d = {
+      list : {},
+      ids : []
+    };
+
+    // For convenience
+    var ejs = ejsResource(config.elasticsearch);
+
+    // Save a reference to this
+    var self = this;
+
+    // Call this whenever we need to reload the important stuff
+    this.init = function() {
+      // Populate defaults
+      _.defaults(dashboard.current.services.filter,_d);
+
+      _.each(dashboard.current.services.filter.list,function(f) {
+        self.set(f,f.id,true);
+      });
+
+    };
+
+    this.ids = function() {
+      return dashboard.current.services.filter.ids;
+    };
+
+    this.list = function() {
+      return dashboard.current.services.filter.list;
+    };
+
+    // This is used both for adding filters and modifying them.
+    // If an id is passed, the filter at that id is updated
+    this.set = function(filter,id,noRefresh) {
+      var _r;
+
+      _.defaults(filter,{
+        mandate:'must',
+        active: true
+      });
+
+      if(!_.isUndefined(id)) {
+        if(!_.isUndefined(dashboard.current.services.filter.list[id])) {
+          _.extend(dashboard.current.services.filter.list[id],filter);
+          _r = id;
+        } else {
+          _r = false;
+        }
+      } else {
+        if(_.isUndefined(filter.type)) {
+          _r = false;
+        } else {
+          var _id = nextId();
+          var _filter = {
+            alias: '',
+            id: _id,
+            mandate: 'must'
+          };
+          _.defaults(filter,_filter);
+          dashboard.current.services.filter.list[_id] = filter;
+          dashboard.current.services.filter.ids.push(_id);
+          _r = _id;
+        }
+      }
+      if(!$rootScope.$$phase) {
+        $rootScope.$apply();
+      }
+      if(noRefresh !== true) {
+        $timeout(function(){
+          dashboard.refresh();
+        },0);
+      }
+      dashboard.current.services.filter.ids = dashboard.current.services.filter.ids =
+        _.intersection(_.map(dashboard.current.services.filter.list,
+          function(v,k){return parseInt(k,10);}),dashboard.current.services.filter.ids);
+      $rootScope.$broadcast('filter');
+
+      return _r;
+    };
+
+    this.remove = function(id,noRefresh) {
+      var _r;
+      if(!_.isUndefined(dashboard.current.services.filter.list[id])) {
+        delete dashboard.current.services.filter.list[id];
+        // This must happen on the full path also since _.without returns a copy
+        dashboard.current.services.filter.ids = dashboard.current.services.filter.ids = _.without(dashboard.current.services.filter.ids,id);
+        _r = true;
+      } else {
+        _r = false;
+      }
+      if(!$rootScope.$$phase) {
+        $rootScope.$apply();
+      }
+      if(noRefresh !== true) {
+        $timeout(function(){
+          dashboard.refresh();
+        },0);
+      }
+      $rootScope.$broadcast('filter');
+      return _r;
+    };
+
+    this.removeByType = function(type,noRefresh) {
+      var ids = self.idsByType(type);
+      _.each(ids,function(id) {
+        self.remove(id,true);
+      });
+      if(noRefresh !== true) {
+        $timeout(function(){
+          dashboard.refresh();
+        },0);
+      }
+      return ids;
+    };
+
+    this.getBoolFilter = function(ids) {
+      var bool = ejs.BoolFilter();
+      // there is no way to introspect the BoolFilter and find out if it has a filter. We must keep note.
+      var added_a_filter = false;
+
+      _.each(ids,function(id) {
+        if(dashboard.current.services.filter.list[id].active) {
+          added_a_filter = true;
+
+          switch(dashboard.current.services.filter.list[id].mandate)
+          {
+          case 'mustNot':
+            bool.mustNot(self.getEjsObj(id));
+            break;
+          case 'either':
+            bool.should(self.getEjsObj(id));
+            break;
+          default:
+            bool.must(self.getEjsObj(id));
+          }
+        }
+      });
+      // add a match filter so we'd get some data
+      if (!added_a_filter) {
+        bool.must(ejs.MatchAllFilter());
+      }
+      return bool;
+    };
+
+    this.getEjsObj = function(id) {
+      return self.toEjsObj(dashboard.current.services.filter.list[id]);
+    };
+
+    this.toEjsObj = function (filter) {
+      if(!filter.active) {
+        return false;
+      }
+      switch(filter.type)
+      {
+      case 'time':
+        var _f = ejs.RangeFilter(filter.field).from(kbn.parseDate(filter.from).valueOf());
+        if(!_.isUndefined(filter.to)) {
+          _f = _f.to(kbn.parseDate(filter.to).valueOf());
+        }
+        return _f;
+      case 'range':
+        return ejs.RangeFilter(filter.field)
+          .from(filter.from)
+          .to(filter.to);
+      case 'querystring':
+        return ejs.QueryFilter(ejs.QueryStringQuery(filter.query)).cache(true);
+      case 'field':
+        return ejs.QueryFilter(ejs.QueryStringQuery(filter.field+":("+filter.query+")")).cache(true);
+      case 'terms':
+        return ejs.TermsFilter(filter.field,filter.value);
+      case 'exists':
+        return ejs.ExistsFilter(filter.field);
+      case 'missing':
+        return ejs.MissingFilter(filter.field);
+      default:
+        return false;
+      }
+    };
+
+    this.getByType = function(type,inactive) {
+      return _.pick(dashboard.current.services.filter.list,self.idsByType(type,inactive));
+    };
+
+    this.idsByType = function(type,inactive) {
+      var _require = inactive ? {type:type} : {type:type,active:true};
+      return _.pluck(_.where(dashboard.current.services.filter.list,_require),'id');
+    };
+
+    // TOFIX: Error handling when there is more than one field
+    this.timeField = function() {
+      return _.pluck(self.getByType('time'),'field');
+    };
+
+    // Parse is used when you need to know about the raw filter
+    this.timeRange = function(parse) {
+      var _t = _.last(_.where(dashboard.current.services.filter.list,{type:'time',active:true}));
+      if(_.isUndefined(_t)) {
+        return false;
+      }
+      if(parse === false) {
+        return {
+          from: _t.from,
+          to: _t.to
+        };
+      } else {
+        var
+          _from = _t.from,
+          _to = _t.to || new Date();
+
+        return {
+          from : kbn.parseDate(_from),
+          to : kbn.parseDate(_to)
+        };
+      }
+    };
+
+    var nextId = function() {
+      var idCount = dashboard.current.services.filter.ids.length;
+      if(idCount > 0) {
+        // Make a sorted copy of the ids array
+        var ids = _.sortBy(_.clone(dashboard.current.services.filter.ids),function(num){
+          return num;
+        });
+        return kbn.smallestMissing(ids);
+      } else {
+        // No ids currently in list
+        return 0;
+      }
+    };
+
+    // Now init
+    self.init();
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/kbnIndex.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/kbnIndex.js b/metron-ui/lib/public/app/services/kbnIndex.js
new file mode 100755
index 0000000..8dbdb85
--- /dev/null
+++ b/metron-ui/lib/public/app/services/kbnIndex.js
@@ -0,0 +1,115 @@
+define([
+  'angular',
+  'lodash',
+  'config',
+  'moment'
+],
+function (angular, _, config, moment) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('kbnIndex', function($http, alertSrv, ejsResource) {
+    // returns a promise containing an array of all indices matching the index
+    // pattern that exist in a given range
+    this.indices = function(from,to,pattern,interval) {
+      var possible = [];
+      _.each(expand_range(from,to,interval),function(d){
+        possible.push(d.utc().format(pattern));
+      });
+
+      return resolve_indices(possible).then(function(p) {
+        // an extra intersection
+        var indices = _.uniq(_.flatten(_.map(possible,function(possibleIndex) {
+          return _.intersection(possibleIndex.split(','),p);
+        })));
+        indices.reverse();
+        return indices;
+      });
+    };
+
+    var ejs = ejsResource(config.elasticsearch);
+
+
+    // returns a promise containing an array of all indices in an elasticsearch
+    // cluster
+    function resolve_indices(indices) {
+      var something;
+      indices = _.uniq(_.map(indices,  encodeURIComponent));
+
+      something = ejs.client.get("/" + indices.join(",") + "/_aliases?ignore_missing=true",
+        undefined, undefined, function (data, p) {
+          if (p === 404) {
+            return [];
+          }
+          else if(p === 0) {
+            alertSrv.set('Error',"Could not contact Elasticsearch at "+ejs.config.server+
+              ". Please ensure that Elasticsearch is reachable from your system." ,'error');
+          } else {
+            alertSrv.set('Error',"Could not reach "+ejs.config.server+"/_aliases. If you"+
+              " are using a proxy, ensure it is configured correctly",'error');
+          }
+          return [];
+        });
+
+      return something.then(function(p) {
+
+        var indices = [];
+        _.each(p, function(v,k) {
+          indices.push(k);
+          // Also add the aliases. Could be expensive on systems with a lot of them
+          _.each(v.aliases, function(v, k) {
+            indices.push(k);
+          });
+        });
+        return indices;
+      });
+    }
+
+    /*
+    // this is stupid, but there is otherwise no good way to ensure that when
+    // I extract the date from an object that I get the UTC date. Stupid js.
+    // I die a little inside every time I call this function.
+    // Update: I just read this again. I died a little more inside.
+    // Update2: More death.
+    function fake_utc(date) {
+      date = moment(date).clone().toDate();
+      return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000));
+    }
+    */
+
+    // Create an array of date objects by a given interval
+    function expand_range(start, end, interval) {
+      if(_.contains(['hour','day','week','month','year'],interval)) {
+        var range;
+        start = moment(start).clone();
+        range = [];
+        while (start.isBefore(end)) {
+          range.push(start.clone());
+          switch (interval) {
+          case 'hour':
+            start.add('hours',1);
+            break;
+          case 'day':
+            start.add('days',1);
+            break;
+          case 'week':
+            start.add('weeks',1);
+            break;
+          case 'month':
+            start.add('months',1);
+            break;
+          case 'year':
+            start.add('years',1);
+            break;
+          }
+        }
+        range.push(moment(end).clone());
+        return range;
+      } else {
+        return false;
+      }
+    }
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/panelMove.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/panelMove.js b/metron-ui/lib/public/app/services/panelMove.js
new file mode 100755
index 0000000..e2d9b04
--- /dev/null
+++ b/metron-ui/lib/public/app/services/panelMove.js
@@ -0,0 +1,68 @@
+define([
+  'angular',
+  'lodash'
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('panelMove', function(dashboard, $rootScope) {
+
+    /* each of these can take event,ui,data parameters */
+
+    this.onStart = function() {
+      dashboard.panelDragging =  true;
+      $rootScope.$apply();
+    };
+
+    this.onOver = function() {
+      $rootScope.$apply();
+    };
+
+    this.onOut = function() {
+      $rootScope.$apply();
+    };
+
+    /*
+      Use our own drop logic. the $parent.$parent this is ugly.
+    */
+    this.onDrop = function(event,ui,data) {
+      var
+        dragRow = data.draggableScope.$parent.$parent.row.panels,
+        dropRow =  data.droppableScope.$parent.$parent.row.panels,
+        dragIndex = data.dragSettings.index,
+        dropIndex =  data.dropSettings.index;
+
+
+      // Remove panel from source row
+      dragRow.splice(dragIndex,1);
+
+      // Add to destination row
+      if(!_.isUndefined(dropRow)) {
+        dropRow.splice(dropIndex,0,data.dragItem);
+      }
+
+      dashboard.panelDragging = false;
+      // Cleanup nulls/undefined left behind
+      cleanup();
+      $rootScope.$apply();
+      $rootScope.$broadcast('render');
+    };
+
+    this.onStop = function() {
+      dashboard.panelDragging = false;
+      cleanup();
+      $rootScope.$apply();
+    };
+
+    var cleanup = function () {
+      _.each(dashboard.current.rows, function(row) {
+        row.panels = _.without(row.panels,{});
+        row.panels = _.compact(row.panels);
+      });
+    };
+
+  });
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/querySrv.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/querySrv.js b/metron-ui/lib/public/app/services/querySrv.js
new file mode 100755
index 0000000..d883de6
--- /dev/null
+++ b/metron-ui/lib/public/app/services/querySrv.js
@@ -0,0 +1,284 @@
+define([
+  'angular',
+  'lodash',
+  'config',
+  'kbn'
+],
+function (angular, _, config, kbn) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('querySrv', function(dashboard, ejsResource, filterSrv, esVersion, $q) {
+
+    // Save a reference to this
+    var self = this;
+
+    // Create an object to hold our service state on the dashboard
+    dashboard.current.services.query = dashboard.current.services.query || {};
+    _.defaults(dashboard.current.services.query,{
+      list : {},
+      ids : [],
+    });
+
+    this.colors = [
+      "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
+      "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
+      "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3
+      "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4
+      "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5
+      "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6
+      "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"  //7
+    ];
+
+    // For convenience
+    var ejs = ejsResource(config.elasticsearch);
+
+    // Holds all actual queries, including all resolved abstract queries
+    var resolvedQueries = [];
+
+    // Defaults for generic query object
+    var _query = {
+      alias: '',
+      pin: false,
+      type: 'lucene',
+      enable: true
+    };
+
+    // Defaults for specific query types
+    var _dTypes = {
+      "lucene": {
+        query: "*"
+      },
+      "regex": {
+        query: ".*"
+      },
+      "topN": {
+        query: "*",
+        field: "_type",
+        size: 5,
+        union: 'AND'
+      }
+    };
+
+    // query type meta data that is not stored on the dashboard object
+    this.queryTypes = {
+      lucene: {
+        require:">=0.17.0",
+        icon: "icon-circle",
+        resolve: function(query) {
+          // Simply returns itself
+          var p = $q.defer();
+          p.resolve(_.extend(query,{parent:query.id}));
+          return p.promise;
+        }
+      },
+      regex: {
+        require:">=0.90.12",
+        icon: "icon-circle",
+        resolve: function(query) {
+          // Simply returns itself
+          var p = $q.defer();
+          p.resolve(_.extend(query,{parent:query.id}));
+          return p.promise;
+        }
+      },
+      topN : {
+        require:">=0.90.3",
+        icon: "icon-cog",
+        resolve: function(q) {
+          var suffix = '';
+          if (q.union === 'AND') {
+            suffix = ' AND (' + (q.query||'*') + ')';
+          } else if (q.union === 'OR') {
+            suffix = ' OR (' + (q.query||'*') + ')';
+          }
+
+          var request = ejs.Request().indices(dashboard.indices);
+          // Terms mode
+          request = request
+            .facet(ejs.TermsFacet('query')
+              .field(q.field)
+              .size(q.size)
+              .facetFilter(ejs.QueryFilter(
+                ejs.FilteredQuery(
+                  ejs.QueryStringQuery(q.query || '*'),
+                  filterSrv.getBoolFilter(filterSrv.ids)
+                  )))).size(0);
+
+          var results = request.doSearch();
+          // Like the regex and lucene queries, this returns a promise
+          return results.then(function(data) {
+            var _colors = kbn.colorSteps(q.color,data.facets.query.terms.length);
+            var i = -1;
+            return _.map(data.facets.query.terms,function(t) {
+              ++i;
+              return self.defaults({
+                query  : q.field+':"'+kbn.addslashes('' + t.term)+'"'+suffix,
+                alias  : t.term + (q.alias ? " ("+q.alias+")" : ""),
+                type   : 'lucene',
+                color  : _colors[i],
+                parent : q.id
+              });
+            });
+          });
+        }
+      }
+    };
+
+    self.types = [];
+    _.each(self.queryTypes,function(type,name){
+      esVersion.is(type.require).then(function(is) {
+        if(is) {
+          self.types.push(name);
+        }
+      });
+    });
+
+
+    this.list = function () {
+      return dashboard.current.services.query.list;
+    };
+
+    this.ids = function () {
+      return dashboard.current.services.query.ids;
+    };
+
+    this.init = function() {
+
+      dashboard.current.services.query.ids =
+        _.intersection(_.map(dashboard.current.services.query.list,
+          function(v,k){return parseInt(k,10);}),self.ids());
+
+      // Check each query object, populate its defaults
+      _.each(dashboard.current.services.query.list,function(query) {
+        query = self.defaults(query);
+      });
+
+      if (dashboard.current.services.query.ids.length === 0) {
+        self.set({});
+      }
+    };
+
+    // This is used both for adding queries and modifying them. If an id is passed,
+    // the query at that id is updated
+    this.set = function(query,id) {
+      if(!_.isUndefined(id)) {
+        if(!_.isUndefined(dashboard.current.services.query.list[id])) {
+          _.extend(dashboard.current.services.query.list[id],query);
+          return id;
+        } else {
+          return false;
+        }
+      } else {
+        // Query must have an id and color already
+        query.id = _.isUndefined(query.id) ? nextId() : query.id;
+        query.color = query.color || colorAt(query.id);
+        // Then it can get defaults
+        query = self.defaults(query);
+        dashboard.current.services.query.list[query.id] = query;
+        dashboard.current.services.query.ids.push(query.id);
+        return query.id;
+      }
+    };
+
+    this.defaults = function(query) {
+      _.defaults(query,_query);
+      _.defaults(query,_dTypes[query.type]);
+      query.color = query.color || colorAt(query.id);
+      return query;
+    };
+
+    this.remove = function(id) {
+      if(!_.isUndefined(dashboard.current.services.query.list[id])) {
+        delete dashboard.current.services.query.list[id];
+        // This must happen on the full path also since _.without returns a copy
+        dashboard.current.services.query.ids = _.without(dashboard.current.services.query.ids,id);
+        return true;
+      } else {
+        return false;
+      }
+    };
+
+
+    // These are the only query types that can be returned by a compound query.
+    this.toEjsObj = function (q) {
+      switch(q.type)
+      {
+      case 'lucene':
+        return ejs.QueryStringQuery(q.query || '*');
+      case 'regex':
+        return ejs.RegexpQuery('_all',q.query);
+      default:
+        return false;
+      }
+    };
+
+    //
+    this.getQueryObjs = function(ids) {
+      if(_.isUndefined(ids)) {
+        return resolvedQueries;
+      } else {
+        return _.flatten(_.map(ids,function(id) {
+          return _.where(resolvedQueries,{parent:id});
+        }));
+      }
+    };
+
+    // BROKEN
+    this.idsByMode = function(config) {
+      switch(config.mode)
+      {
+      case 'all':
+        return _.pluck(_.where(dashboard.current.services.query.list,{enable:true}),'id');
+      case 'pinned':
+        return _.pluck(_.where(dashboard.current.services.query.list,{pin:true,enable:true}),'id');
+      case 'unpinned':
+        return _.pluck(_.where(dashboard.current.services.query.list,{pin:false,enable:true}),'id');
+      case 'selected':
+        return _.intersection(_.pluck(_.where(dashboard.current.services.query.list,{enable:true}),'id'),config.ids);
+      default:
+        return _.pluck(_.where(dashboard.current.services.query.list,{enable:true}),'id');
+      }
+    };
+
+    // This populates the internal query list and returns a promise containing it
+    this.resolve = function() {
+      // Find ids of all abstract queries
+      // Get a list of resolvable ids, constrast with total list to get abstract ones
+      return $q.all(_.map(dashboard.current.services.query.ids,function(q) {
+        return self.queryTypes[dashboard.current.services.query.list[q].type].resolve(
+          _.clone(dashboard.current.services.query.list[q])).then(function(data){
+          return data;
+        });
+      })).then(function(data) {
+        resolvedQueries = _.flatten(data);
+        _.each(resolvedQueries,function(q,i) {
+          q.id = i;
+        });
+        return resolvedQueries;
+      });
+    };
+
+    var nextId = function() {
+      var idCount = dashboard.current.services.query.ids.length;
+      if(idCount > 0) {
+        // Make a sorted copy of the ids array
+        var ids = _.sortBy(_.clone(dashboard.current.services.query.ids),function(num){
+          return num;
+        });
+        return kbn.smallestMissing(ids);
+      } else {
+        // No ids currently in list
+        return 0;
+      }
+    };
+
+    var colorAt = function(id) {
+      return self.colors[id % self.colors.length];
+    };
+
+    self.init();
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/app/services/timer.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/app/services/timer.js b/metron-ui/lib/public/app/services/timer.js
new file mode 100755
index 0000000..128b3f4
--- /dev/null
+++ b/metron-ui/lib/public/app/services/timer.js
@@ -0,0 +1,34 @@
+define([
+  'angular',
+  'lodash'
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('kibana.services');
+
+  module.service('timer', function($timeout) {
+    // This service really just tracks a list of $timeout promises to give us a
+    // method for cancelling them all when we need to
+
+    var timers = [];
+
+    this.register = function(promise) {
+      timers.push(promise);
+      return promise;
+    };
+
+    this.cancel = function(promise) {
+      timers = _.without(timers,promise);
+      $timeout.cancel(promise);
+    };
+
+    this.cancel_all = function() {
+      _.each(timers, function(t){
+        $timeout.cancel(t);
+      });
+      timers = [];
+    };
+  });
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/0648a447/metron-ui/lib/public/config.js
----------------------------------------------------------------------
diff --git a/metron-ui/lib/public/config.js b/metron-ui/lib/public/config.js
new file mode 100755
index 0000000..bc6ac42
--- /dev/null
+++ b/metron-ui/lib/public/config.js
@@ -0,0 +1,82 @@
+/** @scratch /configuration/config.js/1
+ *
+ * == Configuration
+ * config.js is where you will find the core Kibana configuration. This file contains parameter that
+ * must be set before kibana is run for the first time.
+ */
+define(['settings'],
+function (Settings) {
+  "use strict";
+
+  /** @scratch /configuration/config.js/2
+   *
+   * === Parameters
+   */
+  return new Settings({
+
+    /** @scratch /configuration/config.js/5
+     *
+     * ==== elasticsearch
+     *
+     * The URL to your elasticsearch server. You almost certainly don't
+     * want +http://localhost:9200+ here. Even if Kibana and Elasticsearch are on
+     * the same host. By default this will attempt to reach ES at the same host you have
+     * kibana installed on. You probably want to set it to the FQDN of your
+     * elasticsearch host
+     *
+     * Note: this can also be an object if you want to pass options to the http client. For example:
+     *
+     *  +elasticsearch: {server: "http://localhost:9200", withCredentials: true}+
+     *
+     */
+    elasticsearch: location.protocol + '//' + location.host + '/__es',
+
+    /** @scratch /configuration/config.js/5
+     *
+     * ==== default_route
+     *
+     * This is the default landing page when you don't specify a dashboard to load. You can specify
+     * files, scripts or saved dashboards here. For example, if you had saved a dashboard called
+     * `WebLogs' to elasticsearch you might use:
+     *
+     * default_route: '/dashboard/elasticsearch/WebLogs',
+     */
+    default_route     : '/dashboard/file/default.json',
+    // default_route: '/dashboard/elasticsearch/Your Basic Dashboard',
+
+    /** @scratch /configuration/config.js/5
+     *
+     * ==== kibana-int
+     *
+     * The default ES index to use for storing Kibana specific object
+     * such as stored dashboards
+     */
+    kibana_index: "kibana-int",
+
+    /** @scratch /configuration/config.js/5
+     *
+     * ==== panel_name
+     *
+     * An array of panel modules available. Panels will only be loaded when they are defined in the
+     * dashboard, but this list is used in the "add panel" interface.
+     */
+    panel_names: [
+      'histogram',
+      'map',
+      'goal',
+      'table',
+      'filtering',
+      'timepicker',
+      'text',
+      'pcap',
+      'hits',
+      'column',
+      'trends',
+      'bettermap',
+      'query',
+      'terms',
+      'stats',
+      'sparklines'
+    ]
+  });
+});