Merge branch 'tablepanel2' into develop

This commit is contained in:
Torkel Ödegaard 2015-11-09 11:26:06 +01:00
commit 673ae1edc0
34 changed files with 1318 additions and 77 deletions

View File

@ -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;
}
});

View File

@ -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' },

View File

@ -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;

View File

@ -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?)$'));

View File

@ -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) {

View File

@ -3,10 +3,6 @@
<div class="graph-wrapper" ng-class="{'graph-legend-rightside': panel.legend.rightSide}">
<div class="graph-canvas-wrapper">
<!-- <span class="graph&#45;time&#45;info" ng&#45;if="panelMeta.timeInfo"> -->
<!-- <i class="fa fa&#45;clock&#45;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>

View File

@ -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">

View 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();
}
}

View 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>

View 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;
});
};
}
};
}

View 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>

View 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);

View File

@ -0,0 +1,2 @@
<grafana-panel-table-editor>
</grafana-panel-table-editor>

View 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(',&nbsp;');
}
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;
}
}

View 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>');
});
});
});

View 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');
});
});
});
});

View 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;
}
}

View 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}

View File

@ -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 || '*');

View File

@ -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]);
}
}
}

View File

@ -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();
};

View File

@ -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">

View File

@ -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++) {

View File

@ -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: [

View File

@ -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);
});
});
});

View File

@ -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");
});
});
});

View File

@ -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);
});
});

View File

@ -1,4 +1,3 @@
<div ng-include="httpConfigPartialSrc"></div>
<br>

View File

@ -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
View 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;
}
}

View 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;
}