mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'tablepanel2' into develop
This commit is contained in:
commit
673ae1edc0
@ -45,16 +45,25 @@ function (_, $, coreModule) {
|
||||
}
|
||||
|
||||
var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) {
|
||||
_.each(value.submenu, function(item, subIndex) {
|
||||
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
|
||||
memo.push(value.text + ' ' + item.text);
|
||||
});
|
||||
if (!value.submenu) {
|
||||
value.click = 'menuItemSelected(' + index + ')';
|
||||
memo.push(value.text);
|
||||
} else {
|
||||
_.each(value.submenu, function(item, subIndex) {
|
||||
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
|
||||
memo.push(value.text + ' ' + item.text);
|
||||
});
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
$scope.menuItemSelected = function(index, subIndex) {
|
||||
var item = $scope.menuItems[index];
|
||||
$scope.dropdownTypeaheadOnSelect({$item: item, $subItem: item.submenu[subIndex]});
|
||||
var menuItem = $scope.menuItems[index];
|
||||
var payload = {$item: menuItem};
|
||||
if (menuItem.submenu && subIndex !== void 0) {
|
||||
payload.$subItem = menuItem.submenu[subIndex];
|
||||
}
|
||||
$scope.dropdownTypeaheadOnSelect(payload);
|
||||
};
|
||||
|
||||
$input.attr('data-provide', 'typeahead');
|
||||
@ -65,9 +74,10 @@ function (_, $, coreModule) {
|
||||
updater: function (value) {
|
||||
var result = {};
|
||||
_.each($scope.menuItems, function(menuItem) {
|
||||
result.$item = menuItem;
|
||||
|
||||
_.each(menuItem.submenu, function(submenuItem) {
|
||||
if (value === (menuItem.text + ' ' + submenuItem.text)) {
|
||||
result.$item = menuItem;
|
||||
result.$subItem = submenuItem;
|
||||
}
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ function (_) {
|
||||
window_title_prefix : 'Grafana - ',
|
||||
panels : {
|
||||
'graph': { path: 'app/panels/graph', name: 'Graph' },
|
||||
'table': { path: 'app/panels/table', name: 'Table' },
|
||||
'singlestat': { path: 'app/panels/singlestat', name: 'Single stat' },
|
||||
'text': { path: 'app/panels/text', name: 'Text' },
|
||||
'dashlist': { path: 'app/panels/dashlist', name: 'Dashboard list' },
|
||||
|
@ -1,11 +1,46 @@
|
||||
define([
|
||||
'lodash',
|
||||
'app/core/utils/kbn'
|
||||
],
|
||||
function (_, kbn) {
|
||||
'use strict';
|
||||
///<reference path="../headers/common.d.ts" />
|
||||
|
||||
function TimeSeries(opts) {
|
||||
import _ = require('lodash');
|
||||
import kbn = require('app/core/utils/kbn');
|
||||
|
||||
function matchSeriesOverride(aliasOrRegex, seriesAlias) {
|
||||
if (!aliasOrRegex) { return false; }
|
||||
|
||||
if (aliasOrRegex[0] === '/') {
|
||||
var regex = kbn.stringToJsRegex(aliasOrRegex);
|
||||
return seriesAlias.match(regex) != null;
|
||||
}
|
||||
|
||||
return aliasOrRegex === seriesAlias;
|
||||
}
|
||||
|
||||
function translateFillOption(fill) {
|
||||
return fill === 0 ? 0.001 : fill/10;
|
||||
}
|
||||
|
||||
class TimeSeries {
|
||||
datapoints: any;
|
||||
id: string;
|
||||
label: string;
|
||||
alias: string;
|
||||
color: string;
|
||||
valueFormater: any;
|
||||
stats: any;
|
||||
legend: boolean;
|
||||
allIsNull: boolean;
|
||||
decimals: number;
|
||||
scaledDecimals: number;
|
||||
|
||||
lines: any;
|
||||
bars: any;
|
||||
points: any;
|
||||
yaxis: any;
|
||||
zindex: any;
|
||||
stack: any;
|
||||
fillBelowTo: any;
|
||||
transform: any;
|
||||
|
||||
constructor(opts) {
|
||||
this.datapoints = opts.datapoints;
|
||||
this.label = opts.alias;
|
||||
this.id = opts.alias;
|
||||
@ -16,22 +51,7 @@ function (_, kbn) {
|
||||
this.legend = true;
|
||||
}
|
||||
|
||||
function matchSeriesOverride(aliasOrRegex, seriesAlias) {
|
||||
if (!aliasOrRegex) { return false; }
|
||||
|
||||
if (aliasOrRegex[0] === '/') {
|
||||
var regex = kbn.stringToJsRegex(aliasOrRegex);
|
||||
return seriesAlias.match(regex) != null;
|
||||
}
|
||||
|
||||
return aliasOrRegex === seriesAlias;
|
||||
}
|
||||
|
||||
function translateFillOption(fill) {
|
||||
return fill === 0 ? 0.001 : fill/10;
|
||||
}
|
||||
|
||||
TimeSeries.prototype.applySeriesOverrides = function(overrides) {
|
||||
applySeriesOverrides(overrides) {
|
||||
this.lines = {};
|
||||
this.points = {};
|
||||
this.bars = {};
|
||||
@ -64,7 +84,7 @@ function (_, kbn) {
|
||||
}
|
||||
};
|
||||
|
||||
TimeSeries.prototype.getFlotPairs = function (fillStyle) {
|
||||
getFlotPairs(fillStyle) {
|
||||
var result = [];
|
||||
|
||||
this.stats.total = 0;
|
||||
@ -124,18 +144,17 @@ function (_, kbn) {
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
TimeSeries.prototype.updateLegendValues = function(formater, decimals, scaledDecimals) {
|
||||
updateLegendValues(formater, decimals, scaledDecimals) {
|
||||
this.valueFormater = formater;
|
||||
this.decimals = decimals;
|
||||
this.scaledDecimals = scaledDecimals;
|
||||
};
|
||||
}
|
||||
|
||||
TimeSeries.prototype.formatValue = function(value) {
|
||||
formatValue(value) {
|
||||
return this.valueFormater(value, this.decimals, this.scaledDecimals);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return TimeSeries;
|
||||
|
||||
});
|
||||
export = TimeSeries;
|
@ -192,7 +192,7 @@ function($, _) {
|
||||
|
||||
kbn.stringToJsRegex = function(str) {
|
||||
if (str[0] !== '/') {
|
||||
return new RegExp(str);
|
||||
return new RegExp('^' + str + '$');
|
||||
}
|
||||
|
||||
var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
|
||||
|
@ -32,9 +32,9 @@ function (angular, _, $, kbn, dateMath, rangeUtil) {
|
||||
scope.timing.renderEnd = new Date().getTime();
|
||||
};
|
||||
|
||||
this.broadcastRender = function(scope, data) {
|
||||
this.broadcastRender = function(scope, arg1, arg2) {
|
||||
this.setTimeRenderStart(scope);
|
||||
scope.$broadcast('render', data);
|
||||
scope.$broadcast('render', arg1, arg2);
|
||||
this.setTimeRenderEnd(scope);
|
||||
|
||||
if ($rootScope.profilingEnabled) {
|
||||
|
@ -3,10 +3,6 @@
|
||||
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
|
||||
<div class="graph-canvas-wrapper">
|
||||
|
||||
<!-- <span class="graph-time-info" ng-if="panelMeta.timeInfo"> -->
|
||||
<!-- <i class="fa fa-clock-o"></i> {{panelMeta.timeInfo}} -->
|
||||
<!-- </span> -->
|
||||
|
||||
<div ng-if="datapointsWarning" class="datapoints-warning">
|
||||
<span class="small" ng-show="!datapointsCount">
|
||||
No datapoints <tip>No datapoints returned from metric query</tip>
|
||||
|
@ -63,7 +63,7 @@
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
|
||||
<div>
|
||||
<div class="tight-form-container">
|
||||
<div class="tight-form" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
|
68
public/app/panels/table/controller.ts
Normal file
68
public/app/panels/table/controller.ts
Normal file
@ -0,0 +1,68 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular = require('angular');
|
||||
import _ = require('lodash');
|
||||
import moment = require('moment');
|
||||
import PanelMeta = require('app/features/panel/panel_meta');
|
||||
|
||||
import {TableModel} from './table_model';
|
||||
|
||||
export class TablePanelCtrl {
|
||||
|
||||
constructor($scope, $rootScope, $q, panelSrv, panelHelper) {
|
||||
$scope.ctrl = this;
|
||||
$scope.pageIndex = 0;
|
||||
|
||||
$scope.panelMeta = new PanelMeta({
|
||||
panelName: 'Table',
|
||||
editIcon: "fa fa-table",
|
||||
fullscreen: true,
|
||||
metricsEditor: true,
|
||||
});
|
||||
|
||||
$scope.panelMeta.addEditorTab('Options', 'app/panels/table/options.html');
|
||||
$scope.panelMeta.addEditorTab('Time range', 'app/features/panel/partials/panelTime.html');
|
||||
|
||||
var panelDefaults = {
|
||||
targets: [{}],
|
||||
transform: 'timeseries_to_rows',
|
||||
pageSize: 50,
|
||||
showHeader: true,
|
||||
columns: [],
|
||||
fields: []
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
_.defaults($scope.panel, panelDefaults);
|
||||
|
||||
if ($scope.panel.columns.length === 0) {
|
||||
}
|
||||
|
||||
panelSrv.init($scope);
|
||||
};
|
||||
|
||||
$scope.refreshData = function(datasource) {
|
||||
panelHelper.updateTimeRange($scope);
|
||||
|
||||
return panelHelper.issueMetricQuery($scope, datasource)
|
||||
.then($scope.dataHandler, function(err) {
|
||||
$scope.seriesList = [];
|
||||
$scope.render([]);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.dataHandler = function(results) {
|
||||
$scope.dataRaw = results.data;
|
||||
$scope.render();
|
||||
};
|
||||
|
||||
$scope.render = function() {
|
||||
$scope.table = TableModel.transform($scope.dataRaw, $scope.panel);
|
||||
panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw);
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
}
|
||||
}
|
||||
|
158
public/app/panels/table/editor.html
Normal file
158
public/app/panels/table/editor.html
Normal file
@ -0,0 +1,158 @@
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Data</h5>
|
||||
<div class="tight-form-container">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 140px">
|
||||
To Table Transform
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-large tight-form-input"
|
||||
ng-model="panel.transform"
|
||||
ng-options="k as v.description for (k, v) in transformers"
|
||||
ng-change="render()"></select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" ng-if="panel.transform === 'json'">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 140px">
|
||||
Fields
|
||||
</li>
|
||||
<li class="tight-form-item" ng-repeat="field in panel.fields">
|
||||
<i class="pointer fa fa-remove" ng-click="removeJsonField(field)"></i>
|
||||
<span>
|
||||
{{field.name}}
|
||||
</span>
|
||||
</li>
|
||||
<li class="dropdown" dropdown-typeahead="jsonFieldsMenu" dropdown-typeahead-on-select="addJsonField($item, $subItem)">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5>Table Display</h5>
|
||||
<div class="tight-form-container">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
Pagination (Page size)
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input" placeholder="50"
|
||||
empty-to-null ng-model="panel.pageSize" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row" style="margin-top: 20px">
|
||||
<h5>Column Styles</h5>
|
||||
|
||||
<div class="tight-form-container">
|
||||
<div ng-repeat="column in panel.columns">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list pull-right">
|
||||
<li class="tight-form-item last">
|
||||
<i class="fa fa-remove pointer" ng-click="removeColumnStyle(column)"></i>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
Name or regex
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="column.pattern" bs-typeahead="getColumnNames" ng-blur="render()" data-min-length=0 data-items=100 class="input-medium tight-form-input">
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 86px">
|
||||
Type
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input"
|
||||
ng-model="column.type"
|
||||
ng-options="c.value as c.text for c in columnTypes"
|
||||
ng-change="render()"
|
||||
style="width: 150px"
|
||||
></select>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="tight-form-list" ng-if="column.type === 'date'">
|
||||
<li class="tight-form-item">
|
||||
Format
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-large tight-form-input" ng-model="column.dateFormat" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" ng-if="column.type === 'number'">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item text-right" style="width: 93px">
|
||||
Coloring
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input"
|
||||
ng-model="column.colorMode"
|
||||
ng-options="c.value as c.text for c in colorModes"
|
||||
ng-change="render()"
|
||||
style="width: 150px"
|
||||
></select>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Thresholds<tip>Comma seperated values</tip>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input" style="width: 150px" ng-model="column.thresholds" ng-blur="render()" placeholder="0,50,80" array-join></input>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 60px">
|
||||
Colors
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
<spectrum-picker ng-model="column.colors[0]" ng-change="render()" ></spectrum-picker>
|
||||
<spectrum-picker ng-model="column.colors[1]" ng-change="render()" ></spectrum-picker>
|
||||
<spectrum-picker ng-model="column.colors[2]" ng-change="render()" ></spectrum-picker>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<a class="pointer" ng-click="invertColorOrder()">invert order</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" ng-if="column.type === 'number'">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item text-right" style="width: 93px">
|
||||
Unit
|
||||
</li>
|
||||
<li class="dropdown" style="width: 150px"
|
||||
ng-model="column.unit"
|
||||
dropdown-typeahead="unitFormats"
|
||||
dropdown-typeahead-on-select="setUnitFormat(column, $subItem)">
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 86px">
|
||||
Decimals
|
||||
</li>
|
||||
<li style="width: 105px">
|
||||
<input type="number" class="input-mini tight-form-input" ng-model="column.decimals" ng-change="render()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-inverse" style="margin-top: 20px" ng-click="addColumnStyle()">
|
||||
Add column display rule
|
||||
</button>
|
||||
</div>
|
||||
|
110
public/app/panels/table/editor.ts
Normal file
110
public/app/panels/table/editor.ts
Normal file
@ -0,0 +1,110 @@
|
||||
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular = require('angular');
|
||||
import $ = require('jquery');
|
||||
import _ = require('lodash');
|
||||
import kbn = require('app/core/utils/kbn');
|
||||
import moment = require('moment');
|
||||
|
||||
import {transformers} from './transformers';
|
||||
|
||||
export function tablePanelEditor() {
|
||||
'use strict';
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
templateUrl: 'app/panels/table/editor.html',
|
||||
link: function(scope, elem) {
|
||||
scope.transformers = transformers;
|
||||
scope.unitFormats = kbn.getUnitFormats();
|
||||
scope.colorModes = [
|
||||
{text: 'Disabled', value: null},
|
||||
{text: 'Cell', value: 'cell'},
|
||||
{text: 'Value', value: 'value'},
|
||||
{text: 'Row', value: 'row'},
|
||||
];
|
||||
scope.columnTypes = [
|
||||
{text: 'Number', value: 'number'},
|
||||
{text: 'String', value: 'string'},
|
||||
{text: 'Date', value: 'date'},
|
||||
];
|
||||
|
||||
scope.updateJsonFieldsMenu = function(data) {
|
||||
scope.jsonFieldsMenu = [];
|
||||
if (!data || data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var names = {};
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
if (series.type !== 'docs') {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var y = 0; y < series.datapoints.length; y++) {
|
||||
var doc = series.datapoints[y];
|
||||
for (var propName in doc) {
|
||||
names[propName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_.each(names, function(value, key) {
|
||||
scope.jsonFieldsMenu.push({text: key});
|
||||
});
|
||||
};
|
||||
|
||||
scope.updateJsonFieldsMenu(scope.dataRaw);
|
||||
|
||||
scope.$on('render', function(event, table, rawData) {
|
||||
scope.updateJsonFieldsMenu(rawData);
|
||||
});
|
||||
|
||||
scope.addJsonField = function(menuItem) {
|
||||
scope.panel.fields.push({name: menuItem.text});
|
||||
scope.render();
|
||||
};
|
||||
|
||||
scope.removeJsonField = function(field) {
|
||||
scope.panel.fields = _.without(scope.panel.fields, field);
|
||||
scope.render();
|
||||
};
|
||||
|
||||
scope.setUnitFormat = function(column, subItem) {
|
||||
column.unit = subItem.value;
|
||||
scope.render();
|
||||
};
|
||||
|
||||
scope.addColumnStyle = function() {
|
||||
var columnStyleDefaults = {
|
||||
unit: 'short',
|
||||
type: 'number',
|
||||
decimals: 2,
|
||||
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
|
||||
colorMode: null,
|
||||
pattern: '/.*/',
|
||||
thresholds: [],
|
||||
};
|
||||
|
||||
scope.panel.columns.push(angular.copy(columnStyleDefaults));
|
||||
};
|
||||
|
||||
scope.removeColumnStyle = function(col) {
|
||||
scope.panel.columns = _.without(scope.panel.columns, col);
|
||||
};
|
||||
|
||||
scope.getColumnNames = function() {
|
||||
if (!scope.table) {
|
||||
return [];
|
||||
}
|
||||
return _.map(scope.table.columns, function(col: any) {
|
||||
return col.text;
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
24
public/app/panels/table/module.html
Normal file
24
public/app/panels/table/module.html
Normal file
@ -0,0 +1,24 @@
|
||||
<div class="table-panel-wrapper">
|
||||
<grafana-panel>
|
||||
<div class="table-panel-container">
|
||||
<div class="table-panel-header-bg"></div>
|
||||
<div class="table-panel-scroll">
|
||||
<table class="table-panel-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-repeat="col in table.columns">
|
||||
<div class="table-panel-table-header-inner">
|
||||
{{col.text}}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-panel-footer">
|
||||
</div>
|
||||
</grafana-panel>
|
||||
</div>
|
79
public/app/panels/table/module.ts
Normal file
79
public/app/panels/table/module.ts
Normal file
@ -0,0 +1,79 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular = require('angular');
|
||||
import $ = require('jquery');
|
||||
import _ = require('lodash');
|
||||
import kbn = require('app/core/utils/kbn');
|
||||
import moment = require('moment');
|
||||
|
||||
import {TablePanelCtrl} from './controller';
|
||||
import {TableRenderer} from './renderer';
|
||||
import {tablePanelEditor} from './editor';
|
||||
|
||||
export function tablePanel() {
|
||||
'use strict';
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'app/panels/table/module.html',
|
||||
controller: TablePanelCtrl,
|
||||
link: function(scope, elem) {
|
||||
var data;
|
||||
var panel = scope.panel;
|
||||
var formaters = [];
|
||||
|
||||
function getTableHeight() {
|
||||
var panelHeight = scope.height || scope.panel.height || scope.row.height;
|
||||
if (_.isString(panelHeight)) {
|
||||
panelHeight = parseInt(panelHeight.replace('px', ''), 10);
|
||||
}
|
||||
|
||||
return (panelHeight - 40) + 'px';
|
||||
}
|
||||
|
||||
function appendTableRows(tbodyElem) {
|
||||
var renderer = new TableRenderer(panel, data, scope.dashboard.timezone);
|
||||
tbodyElem.empty();
|
||||
tbodyElem.html(renderer.render(0));
|
||||
}
|
||||
|
||||
function appendPaginationControls(footerElem) {
|
||||
var paginationList = $('<ul></ul>');
|
||||
|
||||
var pageCount = data.rows.length / panel.pageSize;
|
||||
for (var i = 0; i < pageCount; i++) {
|
||||
var pageLinkElem = $('<li><a href="#">' + (i+1) + '</a></li>');
|
||||
paginationList.append(pageLinkElem);
|
||||
}
|
||||
|
||||
var nextLink = $('<li><a href="#">»</a></li>');
|
||||
paginationList.append(nextLink);
|
||||
|
||||
footerElem.empty();
|
||||
footerElem.append(paginationList);
|
||||
}
|
||||
|
||||
function renderPanel() {
|
||||
var rootElem = elem.find('.table-panel-scroll');
|
||||
var tbodyElem = elem.find('tbody');
|
||||
var footerElem = elem.find('.table-panel-footer');
|
||||
|
||||
appendTableRows(tbodyElem);
|
||||
|
||||
rootElem.css({'max-height': getTableHeight()});
|
||||
appendPaginationControls(footerElem);
|
||||
}
|
||||
|
||||
scope.$on('render', function(event, renderData) {
|
||||
data = renderData || data;
|
||||
if (!data) {
|
||||
scope.get_data();
|
||||
return;
|
||||
}
|
||||
renderPanel();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
angular.module('grafana.directives').directive('grafanaPanelTable', tablePanel);
|
||||
angular.module('grafana.directives').directive('grafanaPanelTableEditor', tablePanelEditor);
|
2
public/app/panels/table/options.html
Normal file
2
public/app/panels/table/options.html
Normal file
@ -0,0 +1,2 @@
|
||||
<grafana-panel-table-editor>
|
||||
</grafana-panel-table-editor>
|
129
public/app/panels/table/renderer.ts
Normal file
129
public/app/panels/table/renderer.ts
Normal file
@ -0,0 +1,129 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ = require('lodash');
|
||||
import kbn = require('app/core/utils/kbn');
|
||||
import moment = require('moment');
|
||||
|
||||
export class TableRenderer {
|
||||
formaters: any[];
|
||||
colorState: any;
|
||||
|
||||
constructor(private panel, private table, private timezone) {
|
||||
this.formaters = [];
|
||||
this.colorState = {};
|
||||
}
|
||||
|
||||
getColorForValue(value, style) {
|
||||
if (!style.thresholds) { return null; }
|
||||
|
||||
for (var i = style.thresholds.length - 1; i >= 0 ; i--) {
|
||||
if (value >= style.thresholds[i]) {
|
||||
return style.colors[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
createColumnFormater(style) {
|
||||
if (!style) {
|
||||
return v => v;
|
||||
}
|
||||
|
||||
if (style.type === 'date') {
|
||||
return v => {
|
||||
if (_.isArray(v)) { v = v[0]; }
|
||||
var date = moment(v);
|
||||
if (this.timezone === 'utc') {
|
||||
date = date.utc();
|
||||
}
|
||||
return date.format(style.dateFormat);
|
||||
};
|
||||
}
|
||||
|
||||
if (style.type === 'number') {
|
||||
let valueFormater = kbn.valueFormats[style.unit];
|
||||
|
||||
return v => {
|
||||
if (v === null || v === void 0) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (_.isString(v)) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if (style.colorMode) {
|
||||
this.colorState[style.colorMode] = this.getColorForValue(v, style);
|
||||
}
|
||||
|
||||
return valueFormater(v, style.decimals, null);
|
||||
};
|
||||
}
|
||||
|
||||
return v => {
|
||||
if (v === null || v === void 0) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (_.isArray(v)) {
|
||||
v = v.join(', ');
|
||||
}
|
||||
|
||||
return v;
|
||||
};
|
||||
}
|
||||
|
||||
formatColumnValue(colIndex, value) {
|
||||
if (this.formaters[colIndex]) {
|
||||
return this.formaters[colIndex](value);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.panel.columns.length; i++) {
|
||||
let style = this.panel.columns[i];
|
||||
let column = this.table.columns[colIndex];
|
||||
var regex = kbn.stringToJsRegex(style.pattern);
|
||||
if (column.text.match(regex)) {
|
||||
this.formaters[colIndex] = this.createColumnFormater(style);
|
||||
return this.formaters[colIndex](value);
|
||||
}
|
||||
}
|
||||
|
||||
this.formaters[colIndex] = function(v) {
|
||||
return v;
|
||||
};
|
||||
|
||||
return this.formaters[colIndex](value);
|
||||
}
|
||||
|
||||
renderCell(columnIndex, value) {
|
||||
var value = this.formatColumnValue(columnIndex, value);
|
||||
var style = '';
|
||||
if (this.colorState.cell) {
|
||||
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
|
||||
this.colorState.cell = null;
|
||||
}
|
||||
else if (this.colorState.value) {
|
||||
style = ' style="color:' + this.colorState.value + '"';
|
||||
this.colorState.value = null;
|
||||
}
|
||||
|
||||
return '<td' + style + '>' + value + '</td>';
|
||||
}
|
||||
|
||||
render(page) {
|
||||
let endPos = Math.min(this.panel.pageSize, this.table.rows.length);
|
||||
let startPos = 0;
|
||||
var html = "";
|
||||
|
||||
for (var y = startPos; y < endPos; y++) {
|
||||
let row = this.table.rows[y];
|
||||
html += '<tr>';
|
||||
for (var i = 0; i < this.table.columns.length; i++) {
|
||||
html += this.renderCell(i, row[i]);
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
65
public/app/panels/table/specs/renderer_specs.ts
Normal file
65
public/app/panels/table/specs/renderer_specs.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
|
||||
|
||||
import {TableModel} from '../table_model';
|
||||
import {TableRenderer} from '../renderer';
|
||||
|
||||
describe('when rendering table', () => {
|
||||
describe('given 2 columns', () => {
|
||||
var table = new TableModel();
|
||||
table.columns = [
|
||||
{text: 'Time'},
|
||||
{text: 'Value'},
|
||||
{text: 'Colored'}
|
||||
];
|
||||
|
||||
var panel = {
|
||||
pageSize: 10,
|
||||
columns: [
|
||||
{
|
||||
pattern: 'Time',
|
||||
type: 'date',
|
||||
format: 'LLL'
|
||||
},
|
||||
{
|
||||
pattern: 'Value',
|
||||
type: 'number',
|
||||
unit: 'ms',
|
||||
decimals: 3,
|
||||
},
|
||||
{
|
||||
pattern: 'Colored',
|
||||
type: 'number',
|
||||
unit: 'none',
|
||||
decimals: 1,
|
||||
colorMode: 'value',
|
||||
thresholds: [0, 50, 80],
|
||||
colors: ['green', 'orange', 'red']
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var renderer = new TableRenderer(panel, table, 'utc');
|
||||
|
||||
it('time column should be formated', () => {
|
||||
var html = renderer.renderCell(0, 1388556366666);
|
||||
expect(html).to.be('<td>2014-01-01T06:06:06+00:00</td>');
|
||||
});
|
||||
|
||||
it('number column should be formated', () => {
|
||||
var html = renderer.renderCell(1, 1230);
|
||||
expect(html).to.be('<td>1.230 s</td>');
|
||||
});
|
||||
|
||||
it('number style should ignore string values', () => {
|
||||
var html = renderer.renderCell(1, 'asd');
|
||||
expect(html).to.be('<td>asd</td>');
|
||||
});
|
||||
|
||||
it('colored cell should have style', () => {
|
||||
var html = renderer.renderCell(2, 55);
|
||||
expect(html).to.be('<td style="color:orange">55.0</td>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
107
public/app/panels/table/specs/transformers_specs.ts
Normal file
107
public/app/panels/table/specs/transformers_specs.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
|
||||
|
||||
import {TableModel} from '../table_model';
|
||||
|
||||
describe('when transforming time series table', () => {
|
||||
var table;
|
||||
|
||||
describe('given 2 time series', () => {
|
||||
var time = new Date().getTime();
|
||||
var timeSeries = [
|
||||
{
|
||||
target: 'series1',
|
||||
datapoints: [[12.12, time], [14.44, time+1]],
|
||||
},
|
||||
{
|
||||
target: 'series2',
|
||||
datapoints: [[16.12, time]],
|
||||
}
|
||||
];
|
||||
|
||||
describe('timeseries_to_rows', () => {
|
||||
var panel = {transform: 'timeseries_to_rows'};
|
||||
|
||||
beforeEach(() => {
|
||||
table = TableModel.transform(timeSeries, panel);
|
||||
});
|
||||
|
||||
it('should return 3 rows', () => {
|
||||
expect(table.rows.length).to.be(3);
|
||||
expect(table.rows[0][1]).to.be('series1');
|
||||
expect(table.rows[1][1]).to.be('series1');
|
||||
expect(table.rows[2][1]).to.be('series2');
|
||||
expect(table.rows[0][2]).to.be(12.12);
|
||||
});
|
||||
|
||||
it('should return 3 rows', () => {
|
||||
expect(table.columns.length).to.be(3);
|
||||
expect(table.columns[0].text).to.be('Time');
|
||||
expect(table.columns[1].text).to.be('Series');
|
||||
expect(table.columns[2].text).to.be('Value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeseries_to_columns', () => {
|
||||
var panel = {
|
||||
transform: 'timeseries_to_columns'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
table = TableModel.transform(timeSeries, panel);
|
||||
});
|
||||
|
||||
it ('should return 3 columns', () => {
|
||||
expect(table.columns.length).to.be(3);
|
||||
expect(table.columns[0].text).to.be('Time');
|
||||
expect(table.columns[1].text).to.be('series1');
|
||||
expect(table.columns[2].text).to.be('series2');
|
||||
});
|
||||
|
||||
it ('should return 2 rows', () => {
|
||||
expect(table.rows.length).to.be(2);
|
||||
expect(table.rows[0][1]).to.be(12.12);
|
||||
expect(table.rows[0][2]).to.be(16.12);
|
||||
});
|
||||
|
||||
it ('should be undefined when no value for timestamp', () => {
|
||||
expect(table.rows[1][2]).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON Data', () => {
|
||||
var panel = {
|
||||
transform: 'json',
|
||||
fields: [{name: 'timestamp'}, {name: 'message'}]
|
||||
};
|
||||
var rawData = [
|
||||
{
|
||||
type: 'docs',
|
||||
datapoints: [
|
||||
{
|
||||
timestamp: 'time',
|
||||
message: 'message'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
table = TableModel.transform(rawData, panel);
|
||||
});
|
||||
|
||||
it ('should return 2 columns', () => {
|
||||
expect(table.columns.length).to.be(2);
|
||||
expect(table.columns[0].text).to.be('timestamp');
|
||||
expect(table.columns[1].text).to.be('message');
|
||||
});
|
||||
|
||||
it ('should return 2 rows', () => {
|
||||
expect(table.rows.length).to.be(1);
|
||||
expect(table.rows[0][0]).to.be('time');
|
||||
expect(table.rows[0][1]).to.be('message');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
27
public/app/panels/table/table_model.ts
Normal file
27
public/app/panels/table/table_model.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {transformers} from './transformers';
|
||||
|
||||
export class TableModel {
|
||||
columns: any[];
|
||||
rows: any[];
|
||||
|
||||
constructor() {
|
||||
this.columns = [];
|
||||
this.rows = [];
|
||||
}
|
||||
|
||||
static transform(data, panel) {
|
||||
var model = new TableModel();
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return model;
|
||||
}
|
||||
|
||||
var transformer = transformers[panel.transform];
|
||||
if (!transformer) {
|
||||
throw {message: 'Transformer ' + panel.transformer + ' not found'};
|
||||
}
|
||||
|
||||
transformer.transform(data, panel, model);
|
||||
return model;
|
||||
}
|
||||
}
|
102
public/app/panels/table/transformers.ts
Normal file
102
public/app/panels/table/transformers.ts
Normal file
@ -0,0 +1,102 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import moment = require('moment');
|
||||
import _ = require('lodash');
|
||||
|
||||
var transformers = {};
|
||||
|
||||
transformers['timeseries_to_rows'] = {
|
||||
description: 'Time series to rows',
|
||||
transform: function(data, panel, model) {
|
||||
model.columns = [
|
||||
{text: 'Time', type: 'date'},
|
||||
{text: 'Series'},
|
||||
{text: 'Value'},
|
||||
];
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
for (var y = 0; y < series.datapoints.length; y++) {
|
||||
var dp = series.datapoints[y];
|
||||
model.rows.push([dp[1], series.target, dp[0]]);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
transformers['timeseries_to_columns'] = {
|
||||
description: 'Time series to columns',
|
||||
transform: function(data, panel, model) {
|
||||
model.columns.push({text: 'Time', type: 'date'});
|
||||
|
||||
// group by time
|
||||
var points = {};
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
model.columns.push({text: series.target});
|
||||
|
||||
for (var y = 0; y < series.datapoints.length; y++) {
|
||||
var dp = series.datapoints[y];
|
||||
var timeKey = dp[1].toString();
|
||||
|
||||
if (!points[timeKey]) {
|
||||
points[timeKey] = {time: dp[1]};
|
||||
points[timeKey][i] = dp[0];
|
||||
}
|
||||
else {
|
||||
points[timeKey][i] = dp[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var time in points) {
|
||||
var point = points[time];
|
||||
var values = [point.time];
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var value = point[i];
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
model.rows.push(values);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
transformers['annotations'] = {
|
||||
description: 'Annotations',
|
||||
};
|
||||
|
||||
transformers['json'] = {
|
||||
description: 'JSON Data',
|
||||
transform: function(data, panel, model) {
|
||||
var i, y, z;
|
||||
for (i = 0; i < panel.fields.length; i++) {
|
||||
model.columns.push({text: panel.fields[i].name});
|
||||
}
|
||||
|
||||
if (model.columns.length === 0) {
|
||||
model.columns.push({text: 'JSON'});
|
||||
}
|
||||
|
||||
for (i = 0; i < data.length; i++) {
|
||||
var series = data[i];
|
||||
|
||||
for (y = 0; y < series.datapoints.length; y++) {
|
||||
var dp = series.datapoints[y];
|
||||
var values = [];
|
||||
for (z = 0; z < panel.fields.length; z++) {
|
||||
values.push(dp[panel.fields[z].name]);
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
values.push(JSON.stringify(dp));
|
||||
}
|
||||
model.rows.push(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export {transformers}
|
@ -153,8 +153,8 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
});
|
||||
};
|
||||
|
||||
ElasticDatasource.prototype.getQueryHeader = function(timeFrom, timeTo) {
|
||||
var header = {search_type: "count", "ignore_unavailable": true};
|
||||
ElasticDatasource.prototype.getQueryHeader = function(searchType, timeFrom, timeTo) {
|
||||
var header = {search_type: searchType, "ignore_unavailable": true};
|
||||
header.index = this.indexPattern.getIndexList(timeFrom, timeTo);
|
||||
return angular.toJson(header);
|
||||
};
|
||||
@ -163,20 +163,27 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
var payload = "";
|
||||
var target;
|
||||
var sentTargets = [];
|
||||
|
||||
var header = this.getQueryHeader(options.range.from, options.range.to);
|
||||
var headerAdded = false;
|
||||
|
||||
for (var i = 0; i < options.targets.length; i++) {
|
||||
target = options.targets[i];
|
||||
if (target.hide) {return;}
|
||||
|
||||
var esQuery = angular.toJson(this.queryBuilder.build(target));
|
||||
var queryObj = this.queryBuilder.build(target);
|
||||
var esQuery = angular.toJson(queryObj);
|
||||
var luceneQuery = angular.toJson(target.query || '*');
|
||||
// remove inner quotes
|
||||
luceneQuery = luceneQuery.substr(1, luceneQuery.length - 2);
|
||||
esQuery = esQuery.replace("$lucene_query", luceneQuery);
|
||||
|
||||
payload += header + '\n' + esQuery + '\n';
|
||||
if (!headerAdded) {
|
||||
var searchType = queryObj.size === 0 ? 'count' : 'query_then_fetch';
|
||||
var header = this.getQueryHeader(searchType, options.range.from, options.range.to);
|
||||
payload += header + '\n';
|
||||
headerAdded = true;
|
||||
}
|
||||
|
||||
payload += esQuery + '\n';
|
||||
sentTargets.push(target);
|
||||
}
|
||||
|
||||
@ -185,7 +192,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
payload = payload.replace(/\$timeTo/g, options.range.to.valueOf());
|
||||
payload = templateSrv.replace(payload, options.scopedVars);
|
||||
|
||||
return this._post('/_msearch?search_type=count', payload).then(function(res) {
|
||||
return this._post('/_msearch', payload).then(function(res) {
|
||||
return new ElasticResponse(sentTargets, res).getTimeSeries();
|
||||
});
|
||||
};
|
||||
@ -229,7 +236,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
|
||||
ElasticDatasource.prototype.getTerms = function(queryDef) {
|
||||
var range = timeSrv.timeRange();
|
||||
var header = this.getQueryHeader(range.from, range.to);
|
||||
var header = this.getQueryHeader('count', range.from, range.to);
|
||||
var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
|
||||
|
||||
esQuery = esQuery.replace("$lucene_query", queryDef.query || '*');
|
||||
|
@ -173,6 +173,33 @@ function (_, queryDef) {
|
||||
}
|
||||
};
|
||||
|
||||
ElasticResponse.prototype.processHits = function(hits, seriesList) {
|
||||
var series = {target: 'docs', type: 'docs', datapoints: [], total: hits.total};
|
||||
var propName, hit, doc, i;
|
||||
|
||||
for (i = 0; i < hits.hits.length; i++) {
|
||||
hit = hits.hits[i];
|
||||
doc = {
|
||||
_id: hit._id,
|
||||
_type: hit._type,
|
||||
_index: hit._index
|
||||
};
|
||||
|
||||
if (hit._source) {
|
||||
for (propName in hit._source) {
|
||||
doc[propName] = hit._source[propName];
|
||||
}
|
||||
}
|
||||
|
||||
for (propName in hit.fields) {
|
||||
doc[propName] = hit.fields[propName];
|
||||
}
|
||||
series.datapoints.push(doc);
|
||||
}
|
||||
|
||||
seriesList.push(series);
|
||||
};
|
||||
|
||||
ElasticResponse.prototype.getTimeSeries = function() {
|
||||
var seriesList = [];
|
||||
|
||||
@ -182,15 +209,21 @@ function (_, queryDef) {
|
||||
throw { message: response.error };
|
||||
}
|
||||
|
||||
var aggregations = response.aggregations;
|
||||
var target = this.targets[i];
|
||||
var tmpSeriesList = [];
|
||||
if (response.hits) {
|
||||
this.processHits(response.hits, seriesList);
|
||||
}
|
||||
|
||||
this.processBuckets(aggregations, target, tmpSeriesList, {});
|
||||
this.nameSeries(tmpSeriesList, target);
|
||||
if (response.aggregations) {
|
||||
var aggregations = response.aggregations;
|
||||
var target = this.targets[i];
|
||||
var tmpSeriesList = [];
|
||||
|
||||
for (var y = 0; y < tmpSeriesList.length; y++) {
|
||||
seriesList.push(tmpSeriesList[y]);
|
||||
this.processBuckets(aggregations, target, tmpSeriesList, {});
|
||||
this.nameSeries(tmpSeriesList, target);
|
||||
|
||||
for (var y = 0; y < tmpSeriesList.length; y++) {
|
||||
seriesList.push(tmpSeriesList[y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ function (angular, _, queryDef) {
|
||||
$scope.isFirst = $scope.index === 0;
|
||||
$scope.isSingle = metricAggs.length === 1;
|
||||
$scope.settingsLinkText = '';
|
||||
$scope.aggDef = _.findWhere($scope.metricAggTypes, {value: $scope.agg.type});
|
||||
|
||||
if (!$scope.agg.field) {
|
||||
$scope.agg.field = 'select field';
|
||||
@ -53,6 +54,11 @@ function (angular, _, queryDef) {
|
||||
$scope.agg.meta.std_deviation_bounds_lower = true;
|
||||
$scope.agg.meta.std_deviation_bounds_upper = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'raw_document': {
|
||||
$scope.target.metrics = [$scope.agg];
|
||||
$scope.target.bucketAggs = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -65,8 +71,6 @@ function (angular, _, queryDef) {
|
||||
$scope.agg.settings = {};
|
||||
$scope.agg.meta = {};
|
||||
$scope.showOptions = false;
|
||||
|
||||
$scope.validateModel();
|
||||
$scope.onChange();
|
||||
};
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
<li>
|
||||
<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="tight-form-item-large"></metric-segment-model>
|
||||
</li>
|
||||
<li ng-if="agg.type !== 'count'">
|
||||
<li ng-if="aggDef.requiresField">
|
||||
<metric-segment-model property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="tight-form-item-xxlarge"></metric-segment>
|
||||
</li>
|
||||
<li class="tight-form-item last" ng-if="settingsLinkText">
|
||||
|
@ -71,6 +71,16 @@ function (angular) {
|
||||
return filterObj;
|
||||
};
|
||||
|
||||
ElasticQueryBuilder.prototype.documentQuery = function(query) {
|
||||
query.size = 500;
|
||||
query.sort = {};
|
||||
query.sort[this.timeField] = {order: 'desc', unmapped_type: 'boolean'};
|
||||
query.fields = ["*", "_source"];
|
||||
query.script_fields = {},
|
||||
query.fielddata_fields = [this.timeField];
|
||||
return query;
|
||||
};
|
||||
|
||||
ElasticQueryBuilder.prototype.build = function(target) {
|
||||
if (target.rawQuery) {
|
||||
return angular.fromJson(target.rawQuery);
|
||||
@ -96,6 +106,15 @@ function (angular) {
|
||||
}
|
||||
};
|
||||
|
||||
// handle document query
|
||||
if (target.bucketAggs.length === 0) {
|
||||
metric = target.metrics[0];
|
||||
if (metric && metric.type !== 'raw_document') {
|
||||
throw {message: 'Invalid query'};
|
||||
}
|
||||
return this.documentQuery(query, target);
|
||||
}
|
||||
|
||||
nestedAggs = query;
|
||||
|
||||
for (i = 0; i < target.bucketAggs.length; i++) {
|
||||
|
@ -6,14 +6,15 @@ function (_) {
|
||||
|
||||
return {
|
||||
metricAggTypes: [
|
||||
{text: "Count", value: 'count' },
|
||||
{text: "Average", value: 'avg' },
|
||||
{text: "Sum", value: 'sum' },
|
||||
{text: "Max", value: 'max' },
|
||||
{text: "Min", value: 'min' },
|
||||
{text: "Extended Stats", value: 'extended_stats' },
|
||||
{text: "Percentiles", value: 'percentiles' },
|
||||
{text: "Unique Count", value: "cardinality" }
|
||||
{text: "Count", value: 'count', requiresField: false},
|
||||
{text: "Average", value: 'avg', requiresField: true},
|
||||
{text: "Sum", value: 'sum', requiresField: true},
|
||||
{text: "Max", value: 'max', requiresField: true},
|
||||
{text: "Min", value: 'min', requiresField: true},
|
||||
{text: "Extended Stats", value: 'extended_stats', requiresField: true},
|
||||
{text: "Percentiles", value: 'percentiles', requiresField: true},
|
||||
{text: "Unique Count", value: "cardinality", requiresField: true},
|
||||
{text: "Raw Document", value: "raw_document", requiresField: false}
|
||||
],
|
||||
|
||||
bucketAggTypes: [
|
||||
|
@ -80,4 +80,36 @@ describe('ElasticDatasource', function() {
|
||||
expect(body.query.filtered.query.query_string.query).to.be('escape\\:test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When issueing document query', function() {
|
||||
var requestOptions, parts, header;
|
||||
|
||||
beforeEach(function() {
|
||||
ctx.ds = new ctx.service({url: 'http://es.com', index: 'test', jsonData: {}});
|
||||
|
||||
ctx.backendSrv.datasourceRequest = function(options) {
|
||||
requestOptions = options;
|
||||
return ctx.$q.when({data: {responses: []}});
|
||||
};
|
||||
|
||||
ctx.ds.query({
|
||||
range: { from: moment([2015, 4, 30, 10]), to: moment([2015, 5, 1, 10]) },
|
||||
targets: [{ bucketAggs: [], metrics: [{type: 'raw_document'}], query: 'test' }]
|
||||
});
|
||||
|
||||
ctx.$rootScope.$apply();
|
||||
parts = requestOptions.data.split('\n');
|
||||
header = angular.fromJson(parts[0]);
|
||||
});
|
||||
|
||||
it('should set search type to query_then_fetch', function() {
|
||||
expect(header.search_type).to.eql('query_then_fetch');
|
||||
});
|
||||
|
||||
it('should set size', function() {
|
||||
var body = angular.fromJson(parts[1]);
|
||||
expect(body.size).to.be(500);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -411,4 +411,40 @@ describe('ElasticResponse', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Raw documents query', function() {
|
||||
beforeEach(function() {
|
||||
targets = [{ refId: 'A', metrics: [{type: 'raw_document', id: '1'}], bucketAggs: [] }];
|
||||
response = {
|
||||
responses: [{
|
||||
hits: {
|
||||
total: 100,
|
||||
hits: [
|
||||
{
|
||||
_id: '1',
|
||||
_type: 'type',
|
||||
_index: 'index',
|
||||
_source: {sourceProp: "asd"},
|
||||
fields: {fieldProp: "field" },
|
||||
},
|
||||
{
|
||||
_source: {sourceProp: "asd2"},
|
||||
fields: {fieldProp: "field2" },
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
result = new ElasticResponse(targets, response).getTimeSeries();
|
||||
});
|
||||
|
||||
it('should return docs', function() {
|
||||
expect(result.data.length).to.be(1);
|
||||
expect(result.data[0].type).to.be('docs');
|
||||
expect(result.data[0].total).to.be(100);
|
||||
expect(result.data[0].datapoints.length).to.be(2);
|
||||
expect(result.data[0].datapoints[0].sourceProp).to.be("asd");
|
||||
expect(result.data[0].datapoints[0].fieldProp).to.be("field");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -120,4 +120,14 @@ describe('ElasticQueryBuilder', function() {
|
||||
expect(query.aggs["2"].aggs["4"].date_histogram.field).to.be("@timestamp");
|
||||
});
|
||||
|
||||
it('with raw_document metric', function() {
|
||||
var query = builder.build({
|
||||
metrics: [{type: 'raw_document', id: '1'}],
|
||||
timeField: '@timestamp',
|
||||
bucketAggs: [],
|
||||
});
|
||||
|
||||
expect(query.size).to.be(500);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
<div ng-include="httpConfigPartialSrc"></div>
|
||||
<br>
|
||||
|
||||
|
@ -1,19 +1,21 @@
|
||||
@import "type.less";
|
||||
@import "login.less";
|
||||
@import "submenu.less";
|
||||
@import "graph.less";
|
||||
@import "panel_graph.less";
|
||||
@import "panel_dashlist.less";
|
||||
@import "panel_singlestat.less";
|
||||
@import "panel_table.less";
|
||||
@import "bootstrap-tagsinput.less";
|
||||
@import "tables_lists.less";
|
||||
@import "search.less";
|
||||
@import "panel.less";
|
||||
@import "forms.less";
|
||||
@import "singlestat.less";
|
||||
@import "tightform.less";
|
||||
@import "sidemenu.less";
|
||||
@import "navbar.less";
|
||||
@import "gfbox.less";
|
||||
@import "dashlist.less";
|
||||
@import "admin.less";
|
||||
@import "pagination.less";
|
||||
@import "validation.less";
|
||||
@import "fonts.less";
|
||||
@import "tabs.less";
|
||||
|
113
public/less/pagination.less
Normal file
113
public/less/pagination.less
Normal file
@ -0,0 +1,113 @@
|
||||
.pagination {
|
||||
}
|
||||
|
||||
.pagination ul {
|
||||
display: inline-block;
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
.border-radius(@baseBorderRadius);
|
||||
.box-shadow(0 1px 2px rgba(0,0,0,.05));
|
||||
}
|
||||
.pagination ul > li {
|
||||
display: inline; // Remove list-style and block-level defaults
|
||||
}
|
||||
.pagination ul > li > a,
|
||||
.pagination ul > li > span {
|
||||
float: left; // Collapse white-space
|
||||
padding: 4px 12px;
|
||||
line-height: @baseLineHeight;
|
||||
text-decoration: none;
|
||||
background-color: @paginationBackground;
|
||||
border: 1px solid @paginationBorder;
|
||||
border-left-width: 0;
|
||||
}
|
||||
.pagination ul > li > a:hover,
|
||||
.pagination ul > li > a:focus,
|
||||
.pagination ul > .active > a,
|
||||
.pagination ul > .active > span {
|
||||
background-color: @paginationActiveBackground;
|
||||
}
|
||||
.pagination ul > .active > a,
|
||||
.pagination ul > .active > span {
|
||||
color: @grayLight;
|
||||
cursor: default;
|
||||
}
|
||||
.pagination ul > .disabled > span,
|
||||
.pagination ul > .disabled > a,
|
||||
.pagination ul > .disabled > a:hover,
|
||||
.pagination ul > .disabled > a:focus {
|
||||
color: @grayLight;
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
.pagination ul > li:first-child > a,
|
||||
.pagination ul > li:first-child > span {
|
||||
border-left-width: 1px;
|
||||
.border-left-radius(@baseBorderRadius);
|
||||
}
|
||||
.pagination ul > li:last-child > a,
|
||||
.pagination ul > li:last-child > span {
|
||||
.border-right-radius(@baseBorderRadius);
|
||||
}
|
||||
|
||||
|
||||
// Alignment
|
||||
// --------------------------------------------------
|
||||
|
||||
.pagination-centered {
|
||||
text-align: center;
|
||||
}
|
||||
.pagination-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
// Sizing
|
||||
// --------------------------------------------------
|
||||
|
||||
// Large
|
||||
.pagination-large {
|
||||
ul > li > a,
|
||||
ul > li > span {
|
||||
padding: @paddingLarge;
|
||||
font-size: @fontSizeLarge;
|
||||
}
|
||||
ul > li:first-child > a,
|
||||
ul > li:first-child > span {
|
||||
.border-left-radius(@borderRadiusLarge);
|
||||
}
|
||||
ul > li:last-child > a,
|
||||
ul > li:last-child > span {
|
||||
.border-right-radius(@borderRadiusLarge);
|
||||
}
|
||||
}
|
||||
|
||||
// Small and mini
|
||||
.pagination-mini,
|
||||
.pagination-small {
|
||||
ul > li:first-child > a,
|
||||
ul > li:first-child > span {
|
||||
.border-left-radius(@borderRadiusSmall);
|
||||
}
|
||||
ul > li:last-child > a,
|
||||
ul > li:last-child > span {
|
||||
.border-right-radius(@borderRadiusSmall);
|
||||
}
|
||||
}
|
||||
|
||||
// Small
|
||||
.pagination-small {
|
||||
ul > li > a,
|
||||
ul > li > span {
|
||||
padding: @paddingSmall;
|
||||
font-size: @fontSizeSmall;
|
||||
}
|
||||
}
|
||||
// Mini
|
||||
.pagination-mini {
|
||||
ul > li > a,
|
||||
ul > li > span {
|
||||
padding: @paddingMini;
|
||||
font-size: @fontSizeMini;
|
||||
}
|
||||
}
|
88
public/less/panel_table.less
Normal file
88
public/less/panel_table.less
Normal file
@ -0,0 +1,88 @@
|
||||
.table-panel-wrapper {
|
||||
.panel-content {
|
||||
padding: 0;
|
||||
}
|
||||
.panel-title-container {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-panel-scroll {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table-panel-container {
|
||||
padding-top: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-panel-footer {
|
||||
text-align: center;
|
||||
font-size: 80%;
|
||||
line-height: 2px;
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ul > li {
|
||||
display: inline; // Remove list-style and block-level defaults
|
||||
}
|
||||
ul > li > a {
|
||||
float: left; // Collapse white-space
|
||||
padding: 4px 12px;
|
||||
text-decoration: none;
|
||||
border-left-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-panel-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
tr {
|
||||
border-bottom: 2px solid @bodyBackground;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0;
|
||||
|
||||
&:first-child {
|
||||
.table-panel-table-header-inner {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 0 10px 15px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-panel-header-bg {
|
||||
background: @grafanaListAccent;
|
||||
border-top: 2px solid @bodyBackground;
|
||||
border-bottom: 2px solid @bodyBackground;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.table-panel-table-header-inner {
|
||||
padding: 5px 0 5px 15px;
|
||||
text-align: left;
|
||||
color: @blue;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user