Rewrote the stats_table panel data model, to reduce the number of facets/requests done.

Initial row-discovery fase now also facet for all metrics to get averages. The is used to select the rows that will be displaued. Histrograms are done, if needed, only for displayed rows.

This moves from index*metrics facets to #metrics facets. It also alows us to smartly choose which index/node we show when no sorting is active (smart = things with warnings take preference).
This commit is contained in:
Boaz Leskes 2014-01-26 21:52:27 +01:00
parent cec0b790db
commit 442dd9faa2
4 changed files with 372 additions and 163 deletions

View file

@ -19,7 +19,7 @@ module.exports = function (grunt) {
dist: '"https://marvel-stats.elasticsearch.com/"'
},
kibanaPort: grunt.option('port') || 5601,
kibanaHost: 'localhost'
kibanaHost: grunt.option('host') ||'localhost'
};
// more detailed config

View file

@ -71,7 +71,6 @@ function (angular, app, _, $) {
if($scope.panel.source === 'url') {
$http.get($scope.panel.url).then(function(response) {
console.log(response);
var a = $('<a />');
$scope.links = _.filter(response.data.links, function (link) {

View file

@ -34,8 +34,8 @@
}
</style>
<div class="pull-left marvel-header marvel-table" ng-show="rows.length > 0">
<input type="text" class="input-medium" placeholder="Filter {{panel.mode}}..." ng-model="panel.rowFilter"> <span class="count">{{(rows|filter:panel.rowFilter|limitTo:rowLimit).length}} of {{rows.length}} {{panel.mode}}</span> / {{(rows|filter:{'selected':true}).length}} selected / Last 10m </span>
<div class="pull-left marvel-header marvel-table" ng-show="rows.length > 0 || panel.rowFilter">
<input type="text" class="input-medium" placeholder="Filter {{panel.mode}}..." ng-model="panel.rowFilter" ng-change="onRowFilterChange()"> <span class="count">{{rows.length}} of {{_.size(data)}} {{panel.mode}}</span> / {{selectedData().length}} selected / Last 10m </span>
<br>
<span class="small muted pull-right" ng-show="!viewSelect"> <i class="icon-warning"></i> Compact view. Filter {{panel.mode}} to 5 or less, or set the page refresh rate to 2m or greater for more options.</span>
</div>
@ -50,15 +50,15 @@
<th class="pointer" ng-click="set_sort('__name__')">
{{panel.mode}}
<i ng-show="'__name__' == panel.sort[0]" class="pointer link" ng-class="{'icon-chevron-up': panel.sort[1] == 'asc','icon-chevron-down': panel.sort[1] == 'desc'}"></i>
<a ng-disabled="!hasSelected(rows)" id="detail_view_link" ng-href="{{detailViewLink()}}" class="btn btn-mini btn-info pull-right" bs-tooltip="detailViewTip()" data-placement="right">Dashboard</a>
<a ng-disabled="!hasSelected()" id="detail_view_link" ng-href="{{detailViewLink()}}" class="btn btn-mini btn-info pull-right" bs-tooltip="detailViewTip()" data-placement="right">Dashboard</a>
</th>
<th ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels['_global_'][metric.field])" class="pointer" ng-click="set_sort(metric.field)">
<th ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels[metric.field])" class="pointer" ng-click="set_sort(metric.field)">
{{metric.name}}
<i ng-show='metric.field == panel.sort[0]' class="pointer link" ng-class="{'icon-chevron-up': panel.sort[1] == 'asc','icon-chevron-down': panel.sort[1] == 'desc'}"></i>
</th>
</thead>
<tr ng-repeat="row in rows | filter:panel.rowFilter | orderBy:get_sort_value:panel.sort[1]=='desc'|limitTo:rowLimit">
<tr ng-repeat="row in rows">
<td>
<div class="checkbox">
<label>
@ -68,16 +68,16 @@
</label>
</div>
</td>
<td ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels[row.id][metric.field])">
<td ng-repeat="metric in panel.metrics" ng-class="alertClass(row[metric.field].alert_level)">
<div class="marvel-mean pointer" ng-click="rowClick(row,metric)">
<span bo-text="(_.isNull(data[row.id+'_'+metric.field].mean)?0:data[row.id+'_'+metric.field].mean) | metric_format:metric"></span>
<span bo-text="(_.isNull(row[metric.field].mean)?'n/a':row[metric.field].mean) | metric_format:metric"></span>
<br>
<div class="marvel-stats-sparkline" panel='panel' field="metric.field" series="data[row.id+'_'+metric.field+'_history'].series"></div>
<div class="marvel-stats-sparkline" panel='panel' field="metric.field" series="row[metric.field].series"></div>
</div>
<div class="marvel-extended pointer" ng-click="rowClick(row,metric)">
<span>min: <span bo-text="(_.isNull(data[row.id+'_'+metric.field].min)?0:data[row.id+'_'+metric.field].min) | metric_format:metric"></span></span><br>
<span>max: <span bo-text="(_.isNull(data[row.id+'_'+metric.field].max)?0:data[row.id+'_'+metric.field].max) | metric_format:metric"></span></span>
<span>min: <span bo-text="(_.isNull(row[metric.field].min)?'n/a':row[metric.field].min) | metric_format:metric"></span></span><br>
<span>max: <span bo-text="(_.isNull(row[metric.field].max)?'n/a':row[metric.field].max) | metric_format:metric"></span></span>
</div>
</td>
</tr>
@ -89,15 +89,15 @@
<th class="pointer" ng-click="set_sort('__name__')">
{{panel.mode}}
<i ng-show="'__name__' == panel.sort[0]" class="pointer link" ng-class="{'icon-chevron-up': panel.sort[1] == 'asc','icon-chevron-down': panel.sort[1] == 'desc'}"></i>
<a ng-disabled="!hasSelected(rows)" id="detail_view_link" ng-href="{{detailViewLink()}}" class="btn btn-mini btn-info pull-right" bs-tooltip="detailViewTip()" data-placement="right">Dashboard</a>
<a ng-disabled="!hasSelected()" id="detail_view_link" ng-href="{{detailViewLink()}}" class="btn btn-mini btn-info pull-right" bs-tooltip="detailViewTip()" data-placement="right">Dashboard</a>
</th>
<th ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels['_global_'][metric.field])" class="pointer" ng-click="set_sort(metric.field)">
<th ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels[metric.field])" class="pointer" ng-click="set_sort(metric.field)">
{{metric.name}}
<i ng-show='metric.field == panel.sort[0]' class="pointer link" ng-class="{'icon-chevron-up': panel.sort[1] == 'asc','icon-chevron-down': panel.sort[1] == 'desc'}"></i>
</th>
</thead>
<tr ng-repeat="row in rows | filter:panel.rowFilter | orderBy:get_sort_value:panel.sort[1]=='desc'|limitTo:rowLimit">
<tr ng-repeat="row in rows">
<td>
<div class="checkbox">
<label>
@ -106,11 +106,11 @@
<span class="pointer" ng-click="rowClick(row)">{{ row.id }}</span>
</div>
</td>
<td ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels[row.id][metric.field])">
<td ng-repeat="metric in panel.metrics" ng-class="alertClass(row[metric.field].alert_level)">
<div class="pointer" ng-click="rowClick(row,metric)">
<span bo-text="(_.isNull(data[row.id+'_'+metric.field].mean)?0:data[row.id+'_'+metric.field].mean) | metric_format:metric"></span>
<span bo-text="(_.isNull(row[metric.field].mean)?'n/a':row[metric.field].mean) | metric_format:metric"></span>
<div ng-if="sparkLines" class="marvel-stats-sparkline pointer" ng-click="rowClick(row,metric)" panel='panel' field="metric.field" series="data[row.id+'_'+metric.field+'_history'].series"></div>
<div ng-if="sparkLines" class="marvel-stats-sparkline pointer" ng-click="rowClick(row,metric)" panel='panel' field="metric.field" series="row[metric.field].series"></div>
</div>
</td>
</tr>

View file

@ -18,14 +18,15 @@ define([
log_debug = function (msg) {
console.log(msg);
};
} else {
}
else {
log_debug = function () {
};
}
function y_format_metric_value(value, metric) {
// If this isn't a number, change nothing
if (_.isNaN(value) || !_.isFinite(value)) {
if (_.isNaN(value) || !_.isFinite(value) || !_.isNumber(value)) {
return value;
}
if (metric.y_format === 'bytes') {
@ -53,7 +54,14 @@ define([
var _d = {
compact: false,
mode: 'nodes',
sort: ['__name__', 'asc']
sort: null,
// if you have more rows than this number, full view will be disabled
full_view_row_limit_on_high_refresh: 5,
// if you have more nodes/indices than this number, refresh rate will be capped at 2,
data_limit_for_high_refresh: 50,
// disable display names if more than this number of nodes/indices.
data_limit_for_display_names: 50
//
};
_.defaults($scope.panel, _d);
@ -202,32 +210,31 @@ define([
});
});
$scope.$watch('(rows|filter:panel.rowFilter).length', function (l) {
//Compute view based on number of rows
rowsVsRefresh(l);
});
$scope.$watch('dashboard.current.refresh', function () {
var l = $filter('filter')($scope.rows, $scope.panel.rowFilter).length;
rowsVsRefresh(l);
$scope.updateUIFeaturesBasedOnData();
});
var rowsVsRefresh = function (l) {
if (l > 5 && kbn.interval_to_seconds(dashboard.current.refresh || '1y') < 120) {
$scope.updateUIFeaturesBasedOnData = function () {
var l = $scope.rows.length;
if (l > $scope.panel.full_view_row_limit_on_high_refresh
&& kbn.interval_to_seconds(dashboard.current.refresh || '1y') < 120) {
$scope.panel.compact = true;
$scope.sparkLines = true;
$scope.viewSelect = false;
if(l > 50 && kbn.interval_to_seconds(dashboard.current.refresh || '1y') < 120) {
dashboard.set_interval('2m');
alertSrv.set('Refresh rate',
'Due to the large size of your cluster, the refresh rate has been adjusted to 2m',
'info',30000);
}
} else {
}
else {
$scope.viewSelect = true;
$scope.sparkLines = true;
}
if (_.size($scope.data) > $scope.panel.data_limit_for_high_refresh
&& kbn.interval_to_seconds(dashboard.current.refresh || '1y') < 120) {
dashboard.set_interval('2m');
alertSrv.set('Refresh rate',
'Due to the large size of your cluster, the refresh rate has been adjusted to 2m',
'info', 30000);
}
};
$scope.init = function () {
@ -239,20 +246,28 @@ define([
$scope.warnLevels = {};
$scope.rows = [];
$scope.data = {};
$scope.$on('refresh', function () {
$scope.get_rows();
$scope.get_data();
});
$scope.get_rows();
$scope.get_data();
};
$scope.get_mode_filter = function () {
return $scope.ejs.TermFilter("_type", $scope.panel.mode === "nodes" ? "node_stats" : "index_stats");
};
$scope.get_summary_key = function (id, m) {
return id + "_" + m.field;
};
$scope.get_history_key = function (id, m) {
return id + "_" + m.field + "_history";
};
/*
marks the start of data retrieval. returns true if retrieval should continue
or false if not. May schedule future retrival if needed.
or false if not. May schedule future retrieval if needed.
*/
$scope._register_data_start = function () {
@ -279,7 +294,7 @@ define([
// somehow this was not picked up... retry
console.log("Retrying call from " + now);
$scope._pending_data_retrieval = null;
$scope.get_rows();
$scope.get_data();
}
}, 20000);
}
@ -299,16 +314,17 @@ define([
}
log_debug("firing pending retrieval " + $scope._pending_data_retrieval);
$scope._pending_data_retrieval = null;
$scope.get_rows();
$scope.get_data();
}, 5000); // leave 5 second of some breathing air
} else {
}
else {
$scope._ongoing_data_retrieval = null;
}
};
$scope.get_rows = function () {
$scope.get_data = function () {
if (dashboard.indices.length === 0) {
return;
}
@ -320,8 +336,7 @@ define([
var
request,
filter,
results,
facet;
results;
filter = filterSrv.getBoolFilter(filterSrv.ids);
@ -333,54 +348,122 @@ define([
filter.must($scope.get_mode_filter()).must($scope.ejs.RangeFilter('@timestamp').from(to + "-10m/m").to(to + "/m"));
request = $scope.ejs.Request().indices(dashboard.indices).size(0).searchType("count");
facet = $scope.ejs.TermsFacet('terms')
.field($scope.panel.persistent_field)
.size(9999999)
request.query($scope.ejs.FilteredQuery($scope.ejs.MatchAllQuery(), filter));
// timestamp facet to give us the proper time ranges for each node
request.facet($scope.ejs.TermStatsFacet("timestamp")
.keyField($scope.panel.persistent_field).valueField("@timestamp")
.order('term')
.facetFilter(filter);
.size(2000));
if (!$scope.panel.show_hidden) {
facet.regex("[^.].*");
}
request.facet(facet);
_.each($scope.panel.metrics, function (m) {
request.facet($scope.ejs.TermStatsFacet(m.field)
.keyField($scope.panel.persistent_field).valueField(m.field)
.order('term')
.size(2000));
});
results = request.doSearch();
results.then(function (r) {
var newPersistentIds = _.pluck(r.facets.terms.terms, 'term'),
mrequest;
var mrequest, newData;
if (newPersistentIds.length === 0) {
// populate the summary data based on the other facets
newData = {};
_.each(r.facets['timestamp'].terms, function (f) {
if (!$scope.panel.show_hidden && f.term[0] === ".") {
return;
}
newData[f.term] = {
id: f.term,
time_span: (f.max - f.min) / 1000,
selected: ($scope.data[f.term] || {}).selected,
alert_level: 0
};
});
_.each($scope.panel.metrics, function (m) {
_.each(r.facets[m.field].terms, function (f) {
var summary = newData[f.term];
if (!summary) {
return; // filtered
}
var m_summary = {
mean: null,
max: null,
min: null
};
if (m.derivative) {
// no min max, but we can do avg, if we have a timestamp
if (!summary.time_span) {
summary[m.field] = m_summary;
return;
}
m_summary.mean = (f.max - f.min) / summary.time_span;
if (m.scale && m.scale !== 1) {
m_summary.mean /= m.scale;
}
}
else {
m_summary.min = f.min;
m_summary.max = f.max;
m_summary.mean = f.mean;
if (m.scale && m.scale !== 1) {
m_summary.mean /= m.scale;
m_summary.max /= m.scale;
m_summary.min /= m.scale;
}
}
summary[m.field] = m_summary;
m_summary.alert_level = $scope.alertLevel(m, m_summary.mean);
// if (f.term === "index_t_200" && m.field === "primaries.docs.count") {
// m_summary.alert_level = 1;
// }
// if (f.term === "index_t_300" && m.field === "primaries.indexing.index_total") {
// m_summary.alert_level = 2;
// }
if (m_summary.alert_level > summary.alert_level) {
summary.alert_level = m_summary.alert_level;
}
});
});
if (_.isEmpty(newData)) {
// call the get data function so it will clear out all other data related objects.
$scope.get_data([]);
$scope._register_data_end();
$scope.select_display_data_and_enrich(newData);
return;
}
// in all this cases we don't need the display name, short cut it.
if (!$scope.panel.display_field || $scope.panel.display_field === $scope.panel.persistent_field ||
$scope.panel.compact
_.size(newData) > $scope.panel.data_limit_for_display_names
) {
$scope.get_data(_.map(newPersistentIds, function (id) {
return {
display_name: id,
id: id,
// using findWhere here, though its not very efficient
selected: (_.findWhere($scope.rows, {id: id}) || {}).selected
};
}));
_.each(newData, function (s) {
s.display_name = s.id;
});
$scope._register_data_end();
$scope.select_display_data_and_enrich(newData);
return;
}
// go get display names.
mrequest = $scope.ejs.MultiSearchRequest().indices(dashboard.indices);
_.each(newPersistentIds, function (persistentId) {
_.each(newData, function (s) {
var rowRequest = $scope.ejs.Request().filter(filter);
rowRequest.query(
$scope.ejs.ConstantScoreQuery().query(
$scope.ejs.TermQuery($scope.panel.persistent_field, persistentId)
$scope.ejs.TermQuery($scope.panel.persistent_field, s.id)
)
);
rowRequest.size(1).fields(_.unique([ stripRaw($scope.panel.display_field), stripRaw($scope.panel.persistent_field)]));
@ -389,7 +472,7 @@ define([
});
mrequest.doSearch(function (r) {
var newRows = [],
var
hit,
display_name,
persistent_name;
@ -408,104 +491,205 @@ define([
display_name = hit.fields[stripRaw($scope.panel.display_field)];
persistent_name = hit.fields[stripRaw($scope.panel.persistent_field)];
}
newRows.push({
display_name: display_name || persistent_name,
id: persistent_name,
// using findWhere here, though its not very efficient
selected: (_.findWhere($scope.rows, {id: persistent_name}) || {}).selected
});
(newData[persistent_name] || {}).display_name = display_name;
});
$scope.get_data(newRows);
$scope._register_data_end();
$scope.select_display_data_and_enrich(newData);
}, $scope._register_data_end);
}, $scope._register_data_end);
};
$scope.get_data = function (newRows) {
function applyNewData(rows, data) {
$scope.rows = rows;
$scope.data = data;
$scope.updateUIFeaturesBasedOnData();
$scope.panelMeta.loading = false;
$scope.calculateWarnings();
}
$scope.select_display_data_and_enrich = function (newData) {
// Make sure we have everything for the request to complete
if (_.isUndefined(newRows)) {
newRows = $scope.rows;
if (_.isUndefined(newData)) {
newData = $scope.data;
}
if (dashboard.indices.length === 0 || newRows.length === 0) {
$scope.rows = newRows;
$scope.data = {};
$scope.panelMeta.loading = false;
$scope.calculateWarnings();
$scope._register_data_end();
if (dashboard.indices.length === 0 || _.isEmpty(newData)) {
applyNewData([], {});
return;
}
$scope.panelMeta.loading = true;
var
request,
results;
results,
newRows, newRowsIds;
// decide what rows we're showing...
newRowsIds = [];
if ($scope.panel.rowFilter) {
_.each(newData, function (s) {
if (s.id.indexOf($scope.panel.rowFilter) >= 0) {
newRowsIds.push(s.id);
}
});
}
else {
_.each(newData, function (s) {
newRowsIds.push(s.id);
}
);
}
function compareIdBySelection(id1, id2) {
var s1 = newData[id1], s2 = newData[id2];
if (s1.selected && !s2.selected) {
return -1;
}
if (!s1.selected && s2.selected) {
return 1;
}
return 0;
}
function compareIdByAlert(id1, id2) {
var s1 = newData[id1], s2 = newData[id2];
if (s1.alert_level > s2.alert_level) {
return -1;
}
if (s1.alert_level < s2.alert_level) {
return 1;
}
return 0;
}
function compareIdByName(id1, id2) {
var s1 = newData[id1], s2 = newData[id2];
if (s1.display_name < s2.display_name) {
return -1;
}
if (s1.display_name > s2.display_name) {
return 1;
}
return 0;
}
function compareIdByPanelSort(id1, id2) {
var v1 = $scope.get_sort_value(id1, newData),
v2 = $scope.get_sort_value(id2, newData),
r = 0;
if (v1 < v2) {
r = -1;
}
else if (v1 > v2) {
r = 1;
}
if ($scope.panel.sort[1] === "desc") {
r *= -1;
}
return r;
}
function concatSorting() {
var funcs = arguments;
return function (d1, d2) {
for (var i = 0; i < funcs.length; i++) {
var r = funcs[i].call(this, d1, d2);
if (r !== 0) {
return r;
}
}
return 0;
};
}
if ($scope.panel.sort) {
newRowsIds.sort(concatSorting(compareIdByPanelSort, compareIdByAlert, compareIdBySelection));
newRowsIds = newRowsIds.slice(0, $scope.rowLimit);
}
else {
newRowsIds.sort(concatSorting(compareIdBySelection, compareIdByAlert, compareIdByName));
newRowsIds = newRowsIds.slice(0, $scope.rowLimit);
// sort again for visual effect
// sort again for visual placement
newRowsIds.sort(compareIdByName);
}
newRows = _.map(newRowsIds, function (id) {
return newData[id];
});
// now that we have selections, sort by name (if
request = $scope.ejs.Request().indices(dashboard.indices);
var to = filterSrv.timeRange(false).to;
if (to !== "now") {
to = kbn.parseDate(to).valueOf() + "||";
}
var filter = $scope.ejs.BoolFilter()
.must($scope.ejs.RangeFilter('@timestamp').from(to + "-10m/m").to(to + "/m"))
.must($scope.get_mode_filter());
request.query($scope.ejs.FilteredQuery($scope.ejs.MatchAllQuery(), filter)).size(0);
//to = kbn.parseDate(to).valueOf();
_.each(_.pluck(newRows, 'id'), function (id) {
var filter = $scope.ejs.BoolFilter()
.must($scope.ejs.RangeFilter('@timestamp').from(to + "-10m/m").to(to + "/m"))
.must($scope.ejs.TermsFilter($scope.panel.persistent_field, id))
.must($scope.get_mode_filter());
_.each(newRows, function (row) {
_.each($scope.panel.metrics, function (m) {
request = request
.facet($scope.ejs.StatisticalFacet(id + "_" + m.field)
.field(m.field)
.facetFilter(filter));
request = request.facet($scope.ejs.DateHistogramFacet(id + "_" + m.field + "_history")
if (!row[m.field] || row[m.field].series) {
// already have it or the field was not present in the first iteration. Ignore for now.
return;
}
request.facet($scope.ejs.DateHistogramFacet($scope.get_history_key(row.id, m))
.keyField('@timestamp').valueField(m.field).interval('1m')
.facetFilter(filter)).size(0);
.facetFilter($scope.ejs.TermFilter($scope.panel.persistent_field, row.id))
);
});
});
if (!request.facet() || request.facet().length === 0) {
applyNewData(newRows, newData);
return;
}
results = request.doSearch();
// Populate scope when we have results
results.then(function (results) {
$scope.rows = newRows;
$scope.data = normalizeFacetResults(results.facets, newRows, $scope.panel.metrics);
$scope.panelMeta.loading = false;
$scope.calculateWarnings();
$scope._register_data_end();
},
$scope._register_data_end
addHistoryFacetResults(results.facets, newRows, newData, $scope.panel.metrics);
applyNewData(newRows, newData);
}
);
};
var normalizeFacetResults = function (facets, rows, metrics) {
facets = facets || {}; // deal better with no data.
var addHistoryFacetResults = function (facets, rows, data, metrics) {
_.each(metrics, function (m) {
_.each(_.pluck(rows, 'id'), function (id) {
var summary_key = id + "_" + m.field;
var history_key = id + "_" + m.field + "_history";
var summary = facets[summary_key];
if (!summary) {
_.each(rows, function (row) {
var history_key = $scope.get_history_key(row.id, m);
var history_facet = facets[history_key];
if (!history_facet) {
// no data for this chart.
return;
}
var series_data = _.pluck(facets[history_key].entries, m.derivative ? 'min' : 'mean');
var series_time = _.pluck(facets[history_key].entries, 'time');
var series_data = _.pluck(history_facet.entries, m.derivative ? 'min' : 'mean');
var series_time = _.pluck(history_facet.entries, 'time');
var summary = row[m.field];
if (m.scale && m.scale !== 1) {
series_data = _.map(series_data, function (v) {
return v / m.scale;
});
summary.mean /= m.scale;
summary.max /= m.scale;
summary.min /= m.scale;
}
if (m.derivative) {
// update mean to match min & max. Mean is calculated using the entire period's min/max
// this can be different than the calculation here that is based of the min of every small bucket
var _l = series_data.length - 1;
if (_l <= 0) {
summary.mean = null;
@ -515,12 +699,14 @@ define([
summary.mean = (series_data[_l] - series_data[0]) / avg_time;
}
series_data = _.map(series_data, function (p, i) {
var _v;
if (i === 0) {
_v = null;
} else {
}
else {
var _t = ((series_time[i] - series_time[i - 1]) / 1000); // milliseconds -> seconds.
_v = (p - series_data[i - 1]) / _t;
}
@ -533,13 +719,16 @@ define([
summary.min = _.reduce(series_data, function (m, v) {
return m > v && v != null ? v : m;
}, Number.POSITIVE_INFINITY);
if (summary.max === Number.NEGATIVE_INFINITY) {
summary.max = null;
}
if (summary.min === Number.POSITIVE_INFINITY) {
summary.min = null;
}
}
var series = _.zip(series_time, series_data);
facets[summary_key] = summary;
facets[history_key].series = series;
summary.series = _.zip(series_time, series_data);
});
});
@ -547,32 +736,53 @@ define([
return facets;
};
$scope.hasSelected = function (nodes) {
return _.some(nodes, function (n) {
$scope.hasSelected = function (rows) {
return _.some(rows || $scope.data, function (n) {
return n.selected;
});
};
$scope.get_sort_value = function (row) {
if ($scope.panel.sort[0] === '__name__') {
return row.display_name;
$scope.selectedData = function (data) {
return _.filter(data || $scope.data, function (d) {
return d.selected;
});
};
$scope.get_sort_value = function (id, data) {
if (!data) {
data = $scope.data;
}
return $scope.data[row.id + '_' + $scope.panel.sort[0]].mean;
id = data[id];
if ($scope.panel.sort[0] === '__name__') {
return id.display_name;
}
return id[$scope.panel.sort[0]].mean;
};
$scope.set_sort = function (field) {
if ($scope.panel.sort && $scope.panel.sort[0] === field) {
$scope.panel.sort[1] = $scope.panel.sort[1] === "asc" ? "desc" : "asc";
if ($scope.panel.sort[1] === "asc") {
$scope.panel.sort[1] = "desc";
}
else if ($scope.panel.sort[1] === "desc") {
$scope.panel.sort = null;
}
else {
// shouldn't happen, but whatever
$scope.panel.sort[1] = "asc";
}
}
else {
$scope.panel.sort = [field, 'asc'];
}
$scope.select_display_data_and_enrich();
};
$scope.showFullTable = function () {
if ($scope.panel.compact) {
return false;
} else {
}
else {
return true;
}
};
@ -593,14 +803,14 @@ define([
$scope.detailViewLink = function (rows, fields) {
var
query,
time,
show,
from,
to;
query,
time,
show,
from,
to;
if (_.isUndefined(rows)) {
rows = _.where($scope.rows, {selected: true});
rows = $scope.selectedData();
}
rows = _.map(rows, function (row) {
query = $scope.panel.persistent_field + ':"' + row.id + '"';
@ -617,7 +827,8 @@ define([
if (!_.isUndefined(fields)) {
show = "&show=" + fields.join(",");
} else {
}
else {
show = "";
}
@ -643,23 +854,13 @@ define([
};
$scope.calculateWarnings = function () {
$scope.warnLevels = {_global_: {}};
$scope.warnLevels = {};
_.each($scope.panel.metrics, function (metric) {
$scope.warnLevels._global_[metric.field] = 0;
_.each(_.pluck($scope.rows, 'id'), function (id) {
var num, level, summary;
$scope.warnLevels[id] = $scope.warnLevels[id] || {};
summary = $scope.data[id + '_' + metric.field];
if (!summary) {
return; // no data
}
num = summary.mean;
level = $scope.alertLevel(metric, num);
$scope.warnLevels[id][metric.field] = level;
if (level > $scope.warnLevels._global_[metric.field]) {
$scope.warnLevels._global_[metric.field] = level;
$scope.warnLevels[metric.field] = 0;
_.each($scope.data, function (s) {
var level = (s[metric.field] || {}).alert_level;
if (!_.isUndefined(level) && level > $scope.warnLevels[metric.field]) {
$scope.warnLevels[metric.field] = level;
}
});
});
@ -680,7 +881,8 @@ define([
}
if (testAlert(metric.error, num)) {
level = 2;
} else if (testAlert(metric.warning, num)) {
}
else if (testAlert(metric.warning, num)) {
level = 1;
}
@ -688,7 +890,8 @@ define([
var r = Math.random();
if (r > 0.9) {
level = 2;
} else if (r > 0.8) {
}
else if (r > 0.8) {
level = 1;
}
@ -715,7 +918,8 @@ define([
if (s[0] === '<') {
ret.type = "lower_bound";
s = s.substr(1);
} else if (s[0] === '>') {
}
else if (s[0] === '>') {
s = s.substr(1);
}
@ -727,7 +931,7 @@ define([
};
$scope.addMetric = function (panel,metric) {
$scope.addMetric = function (panel, metric) {
metric = metric || {};
metric = metricDefaults(metric);
panel.metrics.push(metric);
@ -760,16 +964,21 @@ define([
index: -1
};
if ($scope._needs_refresh) {
$scope.get_rows();
$scope.get_data();
}
$scope._needs_refresh = false;
$scope.$emit('render');
};
$scope.deleteMetric = function (panel,index) {
$scope.deleteMetric = function (panel, index) {
panel.metrics = _.without(panel.metrics, panel.metrics[index]);
};
$scope.onRowFilterChange = _.debounce(function () {
$scope.$apply(function (scope) {
$scope.select_display_data_and_enrich(scope.data);
});
}, 500);
});
@ -787,7 +996,8 @@ define([
// it is valid
ctrl.$setValidity('alertValue', true);
return scope.parseAlert(viewValue);
} else {
}
else {
// it is invalid, return undefined (no model update)
ctrl.$setValidity('alertValue', false);
return undefined;