Commit 516a8f11 by Torkel Ödegaard

small refactoring, and added config panel that can edit, add/remove targets

parent 54eae2e3
......@@ -43,15 +43,20 @@ require.config({
modernizr: '../vendor/modernizr-2.6.1',
elasticjs: '../vendor/elasticjs/elastic-angular-client',
rq: '../vendor/rq',
setImmediate: '../vendor/setImmediate',
'ts-widget': '../vendor/timeserieswidget/jquery.tswidget',
'ts-graphite-helpers': '../vendor/timeserieswidget/graphite_helpers'
},
shim: {
underscore: {
exports: '_'
},
rq: {
deps: ['setImmediate'],
exports: 'RQ'
},
angular: {
deps: ['jquery'],
exports: 'angular'
......
<div class="editor-row">
<div class="section">
<h5>Values</h5>
<div class="editor-option">
<label class="small">Chart value</label>
<select ng-change="set_refresh(true)" class="input-small" ng-model="panel.mode" ng-options="f for f in ['count','min','mean','max','total']"></select>
<h5>Graphite Targets</h5>
<div ng-repeat="target in panel.targets">
<div class="row-fluid">
<div class="span12">
<input type="text" ng-model="target.target" class="input-large" style="width:95%" ng-change="render()" />
<i ng-click="panel.targets = _.without(panel.targets, target)" class="pointer icon-remove" style="position: relative; top: -5px; left: 5px;"></i>
</div>
<div class="editor-option" ng-show="panel.mode != 'count'">
<label class="small">Value Field <tip>This field must contain a numeric value</tip></label>
<input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-large" ng-model="panel.value_field">
</div>
</div>
<div class="section">
<h5>Transform Series</h5>
<div class="editor-option" ng-show="panel.mode != 'count'">
<label class="small">Scale</label>
<input type="text" class="input-mini" ng-model="panel.scale">
</div>
<div class="editor-option">
<label class="small">Seconds <tip>Normalize intervals to per-second</tip></label><input type="checkbox" ng-model="panel.scaleSeconds" ng-checked="panel.scaleSeconds">
</div>
<div class="editor-option">
<label class="small">Derivative <tip>Plot the change per interval in the series</tip></label><input type="checkbox" ng-model="panel.derivative" ng-checked="panel.derivative" ng-change="set_refresh(true)">
</div>
</div>
</div>
<h5>Time Options</h5>
<div class="editor-row">
<div class="editor-option">
<label class="small">Time Field</label>
<input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.time_field">
</div>
<div class="editor-option">
<label class="small">Time correction</label>
<select ng-model="panel.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
</div>
<div class="editor-option">
<label class="small">Auto-interval</label><input type="checkbox" ng-model="panel.auto_int" ng-checked="panel.auto_int" />
</div>
<div class="editor-option" ng-show='panel.auto_int'>
<label class="small">Resolution <tip>Shoot for this many data points, rounding to sane intervals</tip></label>
<input type="number" class='input-mini' ng-model="panel.resolution" ng-change='set_refresh(true)'/>
</div>
<div class="editor-option" ng-hide='panel.auto_int'>
<label class="small">Interval <tip>Use Elasticsearch date math format (eg 1m, 5m, 1d, 2w, 1y)</tip></label>
<input type="text" class='input-mini' ng-model="panel.interval" ng-change='set_refresh(true)'/>
</div>
<button ng-click="add_target(panel.target);" class="btn btn-success" ng-show="editor.index == 1">Add target</button>
</div>
\ No newline at end of file
define([
'jquery'
'jquery',
'rq',
'config'
],
function ($) {
function ($, RQ, config) {
'use strict';
String.prototype.graphiteGlob = function(glob) {
var regex = '^';
for (var i = 0; i < glob.length; i++ ) {
var c = glob.charAt(i);
switch (c) {
case '*':
regex += '[^\.]+';
break;
case '.':
regex += '\\.';
break;
default:
regex += c;
}
}
regex += '$';
return this.match(regex);
}
function build_graphite_options(options, raw) {
raw = raw || false;
......@@ -75,77 +59,36 @@ function ($) {
return clean_options;
}
function build_graphite_url(options) {
var limit = 2000; // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
var url = options.graphite_url + "?";
options = build_graphite_options(options, false);
$.map(options, function(option) {
if (url.length + option.length < limit) {
url += '&' + option;
}
});
return url.replace(/\?&/, "?");
}
function find_definition (target_graphite, options) {
var matching_i = undefined;
for (var cfg_i = 0; cfg_i < options.targets.length && matching_i == undefined; cfg_i++) {
// string match (no globbing)
if(options.targets[cfg_i].target == target_graphite.target) {
matching_i = cfg_i;
}
// glob match?
else if(target_graphite.target.graphiteGlob(options.targets[cfg_i].target)) {
matching_i = cfg_i;
}
function loadGraphiteData(requestion, options)
{
var graphOptions = {
from: $.plot.formatDate(options.range.from, '%H%:%M_%Y%m%d'),
until: $.plot.formatDate(options.range.to, '%H%:%M_%Y%m%d'),
targets: options.targets
}
if (matching_i == undefined) {
console.error ("internal error: could not figure out which target_option target_graphite '" +
target_graphite.target + "' comes from");
return [];
}
return options.targets[matching_i];
}
function add_targets(options, response_data) {
var all_targets = [];
for (var res_i = 0; res_i < response_data.length; res_i++) {
var target = find_definition(response_data[res_i], options);
target.label = target.name; // flot wants 'label'
target.data = [];
var nulls = 0;
var non_nulls = 0;
for (var i in response_data[res_i].datapoints) {
if(response_data[res_i].datapoints[i][0] == null) {
nulls++;
if('drawNullAsZero' in options && options['drawNullAsZero']) {
response_data[res_i].datapoints[i][0] = 0;
} else {
// don't tell flot about null values, it prevents adjacent non-null values from
// being rendered correctly
continue;
}
} else {
non_nulls++;
}
target.data.push([response_data[res_i].datapoints[i][1] * 1000, response_data[res_i].datapoints[i][0]]);
}
if (nulls/non_nulls > 0.3) {
console.log("warning: rendered target contains " + nulls + " null values, " + non_nulls + " non_nulls");
}
all_targets.push(target);
var graphiteParameters = build_graphite_options(graphOptions, true);
getGraphiteData(graphiteParameters)
.done(function(data) {
requestion(data);
})
.fail(function() {
requestion(null, 'Error in ajax call to graphite');
});
}
return all_targets;
function getGraphiteData(parameters) {
return $.ajax({
accepts: { text: 'application/json' },
cache: false,
dataType: 'json',
url: config.graphiteUrl,
type: "POST",
data: parameters.join('&')
});
}
return {
build_graphite_options: build_graphite_options,
build_graphite_url: build_graphite_url,
add_targets: add_targets
loadGraphiteData: loadGraphiteData
};
});
\ No newline at end of file
......@@ -19,8 +19,7 @@ define([
'kbn',
'moment',
'./timeSeries',
'./graphiteUtil',
'config',
'./graphiteSrv',
'jquery.flot',
'jquery.flot.events',
'jquery.flot.selection',
......@@ -29,7 +28,7 @@ define([
'jquery.flot.stack',
'jquery.flot.stackpercent'
],
function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
function (angular, app, $, _, kbn, moment, timeSeries, graphiteSrv) {
'use strict';
......@@ -50,11 +49,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
{
title:'Style',
src:'app/panels/graphite/styleEditor.html'
},
{
title:'Queries',
src:'app/panels/graphite/queriesEditor.html'
},
}
],
status : "Stable",
description : "A bucketed time series chart of the current query or queries. Uses the "+
......@@ -169,10 +164,6 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
*/
legend : true,
/** @scratch /panels/histogram/3
* show_query:: If no alias is set, should the query be displayed?
*/
show_query : true,
/** @scratch /panels/histogram/3
* interactive:: Enable click-and-drag to zoom functionality
*/
interactive : true,
......@@ -197,16 +188,11 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
/** @scratch /panels/histogram/3
* derivative:: Show each point on the x-axis as the change from the previous point
*/
derivative : false,
/** @scratch /panels/histogram/3
* tooltip object::
* tooltip.value_type::: Individual or cumulative controls how tooltips are display on stacked charts
* tooltip.query_as_alias::: If no alias is set, should the query be displayed?
*/
tooltip : {
value_type: 'cumulative',
query_as_alias: true
}
},
targets: []
};
_.defaults($scope.panel,_d);
......@@ -215,7 +201,6 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
_.defaults($scope.panel.grid,_d.grid);
$scope.init = function() {
// Hide view options by default
$scope.options = false;
......@@ -268,33 +253,6 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
return $scope.panel.interval;
};
var graphOptions = {
until: 'now',
targets: [
{
name: 'series 1',
color: '#CC6699',
target: "summarize(sum(prod.apps.tradera_site.*.counters.global.request_status.code_404.count), '30s')",
//target: 'integral(prod.apps.touchweb.snake.counters.login.success.count)',
//target: "randomWalk('random1')",
}
]
};
$scope.getGraphiteData = function (options, parameters) {
return $.ajax({
accepts: { text: 'application/json' },
cache: false,
dataType: 'json',
url: config.graphiteUrl,
type: "POST",
data: parameters.join('&'),
error: function(xhr, textStatus, errorThrown) {
$scope.panel.error = 'Failed to do graphite POST request: ' + textStatus + ' : ' + errorThrown;
}
});
};
$scope.colors = [
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
......@@ -317,18 +275,24 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
$scope.get_data = function() {
delete $scope.panel.error;
$scope.panelMeta.loading = true;
var range = $scope.get_time_range();
var interval = $scope.get_interval(range);
graphOptions.from = $.plot.formatDate(range.from, '%H%:%M_%Y%m%d');
var graphiteLoadOptions = {
range: range,
targets: $scope.panel.targets
};
$scope.panelMeta.loading = true;
var result = RQ.sequence([graphiteSrv.loadGraphiteData]);
var graphiteParameters = graphiteUtil.build_graphite_options(graphOptions, true);
var request = $scope.getGraphiteData(graphOptions, graphiteParameters);
$scope.populate_modal(graphiteParameters);
result(function (results, failure) {
if (failure || !results) {
$scope.panel.error = 'Failed to do fetch graphite data: ' + failure;
return;
}
request.done(function(results) {
$scope.data = [];
$scope.panelMeta.loading = false;
......@@ -339,7 +303,7 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
console.log('Data from graphite:', results);
var tsOpts = {
interval: "30s",
interval: interval,
start_date: range && range.from,
end_date: range && range.to,
fill_style: 'connect'
......@@ -380,7 +344,13 @@ function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil, config) {
// Tell the histogram directive to render.
$scope.$emit('render');
});
}, graphiteLoadOptions);
};
$scope.add_target = function() {
$scope.panel.targets.push({target: ''});
};
// function $scope.zoom
......
<h4>Charted</h4>
<div ng-include src="'app/partials/querySelect.html'"></div>
<div class="editor-row">
<h4>Markers</h4>
<div class="small">
Here you can specify a query to be plotted on your chart as a marker. Hovering over a marker will display the field you specify below. If more documents are found than the limit you set, they will be scored by Elasticsearch and events that best match your query will be displayed.
</div>
<style>
.querySelect .query {
margin-right: 5px;
}
.querySelect .selected {
border: 3px solid;
}
.querySelect .unselected {
border: 0px solid;
}
</style>
<p>
<div class="editor-option">
<label class="small">Enable</label>
<input type="checkbox" ng-change="set_refresh(true)" ng-model="panel.annotate.enable" ng-checked="panel.annotate.enable">
</div>
<div class="editor-option" ng-show="panel.annotate.enable">
<label class="small">Marker Query</label>
<input type="text" ng-change="set_refresh(true)" class="input-large" ng-model="panel.annotate.query"/>
</div>
<div class="editor-option" ng-show="panel.annotate.enable">
<label class="small">Tooltip field</label>
<input type="text" class="input-small" ng-model="panel.annotate.field" bs-typeahead="fields.list"/>
</div>
<div class="editor-option" ng-show="panel.annotate.enable">
<label class="small">Limit <tip>Max markers on the chart</tip></label>
<input type="number" class="input-mini" ng-model="panel.annotate.size" ng-change="set_refresh(true)"/>
</div>
<div class="editor-option" ng-show="panel.annotate.enable">
<label class="small">Sort <tip>Determine the most relevant markers using this field</tip></label>
<input type="text" class="input-small" bs-typeahead="fields.list" ng-model="panel.annotate.sort[0]" ng-change="set_refresh(true)" />
<i ng-click="panel.annotate.sort[1] = _.toggle(panel.annotate.sort[1],'desc','asc');set_refresh(true)" ng-class="{'icon-chevron-up': panel.annotate.sort[1] == 'asc','icon-chevron-down': panel.annotate.sort[1] == 'desc'}"></i>
</div>
</div>
(function (global, undefined) {
"use strict";
var tasks = (function () {
function Task(handler, args) {
this.handler = handler;
this.args = args;
}
Task.prototype.run = function () {
// See steps in section 5 of the spec.
if (typeof this.handler === "function") {
// Choice of `thisArg` is not in the setImmediate spec; `undefined` is in the setTimeout spec though:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html
this.handler.apply(undefined, this.args);
} else {
var scriptSource = "" + this.handler;
/*jshint evil: true */
eval(scriptSource);
}
};
var nextHandle = 1; // Spec says greater than zero
var tasksByHandle = {};
var currentlyRunningATask = false;
return {
addFromSetImmediateArguments: function (args) {
var handler = args[0];
var argsToHandle = Array.prototype.slice.call(args, 1);
var task = new Task(handler, argsToHandle);
var thisHandle = nextHandle++;
tasksByHandle[thisHandle] = task;
return thisHandle;
},
runIfPresent: function (handle) {
// From the spec: "Wait until any invocations of this algorithm started before this one have completed."
// So if we're currently running a task, we'll need to delay this invocation.
if (!currentlyRunningATask) {
var task = tasksByHandle[handle];
if (task) {
currentlyRunningATask = true;
try {
task.run();
} finally {
delete tasksByHandle[handle];
currentlyRunningATask = false;
}
}
} else {
// Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a
// "too much recursion" error.
global.setTimeout(function () {
tasks.runIfPresent(handle);
}, 0);
}
},
remove: function (handle) {
delete tasksByHandle[handle];
}
};
}());
function canUseNextTick() {
// Don't get fooled by e.g. browserify environments.
return typeof process === "object" &&
Object.prototype.toString.call(process) === "[object process]";
}
function canUseMessageChannel() {
return !!global.MessageChannel;
}
function canUsePostMessage() {
// The test against `importScripts` prevents this implementation from being installed inside a web worker,
// where `global.postMessage` means something completely different and can't be used for this purpose.
if (!global.postMessage || global.importScripts) {
return false;
}
var postMessageIsAsynchronous = true;
var oldOnMessage = global.onmessage;
global.onmessage = function () {
postMessageIsAsynchronous = false;
};
global.postMessage("", "*");
global.onmessage = oldOnMessage;
return postMessageIsAsynchronous;
}
function canUseReadyStateChange() {
return "document" in global && "onreadystatechange" in global.document.createElement("script");
}
function installNextTickImplementation(attachTo) {
attachTo.setImmediate = function () {
var handle = tasks.addFromSetImmediateArguments(arguments);
process.nextTick(function () {
tasks.runIfPresent(handle);
});
return handle;
};
}
function installMessageChannelImplementation(attachTo) {
var channel = new global.MessageChannel();
channel.port1.onmessage = function (event) {
var handle = event.data;
tasks.runIfPresent(handle);
};
attachTo.setImmediate = function () {
var handle = tasks.addFromSetImmediateArguments(arguments);
channel.port2.postMessage(handle);
return handle;
};
}
function installPostMessageImplementation(attachTo) {
// Installs an event handler on `global` for the `message` event: see
// * https://developer.mozilla.org/en/DOM/window.postMessage
// * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages
var MESSAGE_PREFIX = "com.bn.NobleJS.setImmediate" + Math.random();
function isStringAndStartsWith(string, putativeStart) {
return typeof string === "string" && string.substring(0, putativeStart.length) === putativeStart;
}
function onGlobalMessage(event) {
// This will catch all incoming messages (even from other windows!), so we need to try reasonably hard to
// avoid letting anyone else trick us into firing off. We test the origin is still this window, and that a
// (randomly generated) unpredictable identifying prefix is present.
if (event.source === global && isStringAndStartsWith(event.data, MESSAGE_PREFIX)) {
var handle = event.data.substring(MESSAGE_PREFIX.length);
tasks.runIfPresent(handle);
}
}
if (global.addEventListener) {
global.addEventListener("message", onGlobalMessage, false);
} else {
global.attachEvent("onmessage", onGlobalMessage);
}
attachTo.setImmediate = function () {
var handle = tasks.addFromSetImmediateArguments(arguments);
// Make `global` post a message to itself with the handle and identifying prefix, thus asynchronously
// invoking our onGlobalMessage listener above.
global.postMessage(MESSAGE_PREFIX + handle, "*");
return handle;
};
}
function installReadyStateChangeImplementation(attachTo) {
attachTo.setImmediate = function () {
var handle = tasks.addFromSetImmediateArguments(arguments);
// Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
// into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
var scriptEl = global.document.createElement("script");
scriptEl.onreadystatechange = function () {
tasks.runIfPresent(handle);
scriptEl.onreadystatechange = null;
scriptEl.parentNode.removeChild(scriptEl);
scriptEl = null;
};
global.document.documentElement.appendChild(scriptEl);
return handle;
};
}
function installSetTimeoutImplementation(attachTo) {
attachTo.setImmediate = function () {
var handle = tasks.addFromSetImmediateArguments(arguments);
global.setTimeout(function () {
tasks.runIfPresent(handle);
}, 0);
return handle;
};
}
if (!global.setImmediate) {
// If supported, we should attach to the prototype of global, since that is where setTimeout et al. live.
var attachTo = typeof Object.getPrototypeOf === "function" && "setTimeout" in Object.getPrototypeOf(global) ?
Object.getPrototypeOf(global)
: global;
if (canUseNextTick()) {
// For Node.js before 0.9
installNextTickImplementation(attachTo);
} else if (canUsePostMessage()) {
// For non-IE10 modern browsers
installPostMessageImplementation(attachTo);
} else if (canUseMessageChannel()) {
// For web workers, where supported
installMessageChannelImplementation(attachTo);
} else if (canUseReadyStateChange()) {
// For IE 6–8
installReadyStateChangeImplementation(attachTo);
} else {
// For older browsers
installSetTimeoutImplementation(attachTo);
}
attachTo.clearImmediate = tasks.remove;
}
}(typeof global === "object" && global ? global : this));
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment