Commit 048ac308 by Marcus Efraimsson

Merge branch 'master' into feature/add_es_alerting

parents 7cb0403f 79575ea1
......@@ -13,6 +13,10 @@
* **Security**: Fix XSS vulnerabilities in dashboard links [#11813](https://github.com/grafana/grafana/pull/11813)
* **Singlestat**: Fix "time of last point" shows local time when dashboard timezone set to UTC [#10338](https://github.com/grafana/grafana/issues/10338)
* **Prometheus**: Add support for passing timeout parameter to Prometheus [#11788](https://github.com/grafana/grafana/pull/11788), thx [@mtanda](https://github.com/mtanda)
* **Login**: Add optional option sign out url for generic oauth [#9847](https://github.com/grafana/grafana/issues/9847), thx [@roidelapluie](https://github.com/roidelapluie)
* **Login**: Use proxy server from environment variable if available [#9703](https://github.com/grafana/grafana/issues/9703), thx [@iyeonok](https://github.com/iyeonok)
* **Invite users**: Friendlier error message when smtp is not configured [#12087](https://github.com/grafana/grafana/issues/12087), thx [@thurt](https://github.com/thurt)
* **Graphite**: Don't send distributed tracing headers when using direct/browser access mode [#11494](https://github.com/grafana/grafana/issues/11494)
# 5.1.3 (2018-05-16)
......
# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine
elasticsearch6:
image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4
command: elasticsearch
ports:
- "11200:9200"
- "11300:9300"
fake-elastic6-data:
image: grafana/fake-data-gen
network_mode: bridge
environment:
FD_DATASOURCE: elasticsearch6
FD_PORT: 11200
script.inline: on
script.indexed: on
......@@ -14,11 +14,53 @@ weight = 2
<img class="screenshot" src="/assets/img/features/table-panel.png">
The new table panel is very flexible, supporting both multiple modes for time series as well as for
The table panel is very flexible, supporting both multiple modes for time series as well as for
table, annotation and raw JSON data. It also provides date formatting and value formatting and coloring options.
To view table panels in action and test different configurations with sample data, check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase).
## Querying Data
The table panel displays the results of a query specified in the **Metrics** tab.
The result being displayed depends on the datasource and the query, but generally there is one row per datapoint, with extra columns for associated keys and values, as well as one column for the numeric value of the datapoint.
You can change the behavior in the section **Data to Table** below.
### Merge Multiple Queries per Table
> Only available in Grafana v5.0+.
Sometimes it is useful to display the results of multiple queries in the same table on corresponding rows, e.g., when comparing capacity and actual usage of resources.
In this example usage and capacity are metrics that will have corresponding datapoints, while their associated keys and values can be used to match them.
(This matching is only available with the **Table Transform** set to **Table**.)
In its simplest case, both queries return time-series data with a numeric value and a timestamp.
If the timestamps are the same, datapoints will be matched and rendered on the same row.
Some datasources return keys and values (labels, tags) associated with the datapoint.
These are being matched as well if they are present in both results and have the same value.
The following datapoints will end up on the same row with one time column, two label columns ("host" and "job") and two value columns:
```
Datapoint for query A: {time: 1, host: "node-2", job: "job-8", value: 3}
Datapoint for query B: {time: 1, host: "node-2", value: 4}
```
The following two results cannot be matched and will be rendered on separate rows:
```
Different time
Datapoint for query A: {time: 1, host: "node-2", job: "job-8", value: 3}
Datapoint for query B: {time: 2, host: "node-2", value: 4}
Different label "host"
Datapoint for query A: {time: 1, host: "node-2", job: "job-8", value: 3}
Datapoint for query B: {time: 1, host: "node-9", value: 4}
```
You can still merge both of the above cases by changing the conflicting column's **Type** to **hidden** in the **Column Styles**.
Note that if each datapoint of your query results have multiple value fields like max, min, mean, etc., they will likely have different values and therefore will not match and render on separate rows.
If you intend for rows to be merged but see them rendered on separate rows, check the query results in the **Query Inspector** for field values being identical across datapoints that should be merged into a row.
## Options overview
The table panel has many ways to manipulate your data for optimal presentation.
......
......@@ -134,11 +134,11 @@
},
"license": "Apache-2.0",
"dependencies": {
"angular": "^1.6.6",
"angular": "1.6.6",
"angular-bindonce": "^0.3.1",
"angular-native-dragdrop": "^1.2.2",
"angular-route": "^1.6.6",
"angular-sanitize": "^1.6.6",
"angular-route": "1.6.6",
"angular-sanitize": "1.6.6",
"babel-polyfill": "^6.26.0",
"baron": "^3.0.3",
"brace": "^0.10.0",
......@@ -161,7 +161,7 @@
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-grid-layout-grafana": "0.16.0",
"react-grid-layout": "0.16.6",
"react-highlight-words": "^0.10.0",
"react-popper": "^0.7.5",
"react-select": "^1.1.0",
......@@ -180,4 +180,4 @@
"resolutions": {
"caniuse-db": "1.0.30000772"
}
}
\ No newline at end of file
}
......@@ -78,6 +78,7 @@ func OAuthLogin(ctx *m.ReqContext) {
// handle call back
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: setting.OAuthService.OAuthInfos[name].TlsSkipVerify,
},
......
......@@ -74,6 +74,9 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
}
if err := bus.Dispatch(&emailCmd); err != nil {
if err == m.ErrSmtpNotEnabled {
return Error(412, err.Error(), err)
}
return Error(500, "Failed to send email invite", err)
}
......
......@@ -230,8 +230,8 @@ func parseMultiSelectValue(input string) []string {
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
regions := []string{
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1",
"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1",
"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
}
result := make([]suggestData, 0)
......
......@@ -144,10 +144,10 @@ func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) {
if timeIndex >= 0 {
switch value := values[timeIndex].(type) {
case time.Time:
values[timeIndex] = EpochPrecisionToMs(float64(value.UnixNano()))
values[timeIndex] = float64(value.UnixNano()) / float64(time.Millisecond)
case *time.Time:
if value != nil {
values[timeIndex] = EpochPrecisionToMs(float64((*value).UnixNano()))
values[timeIndex] = float64((*value).UnixNano()) / float64(time.Millisecond)
}
case int64:
values[timeIndex] = int64(EpochPrecisionToMs(float64(value)))
......
......@@ -12,14 +12,17 @@ import (
func TestSqlEngine(t *testing.T) {
Convey("SqlEngine", t, func() {
dt := time.Date(2018, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC)
earlyDt := time.Date(1970, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC)
Convey("Given row values with time.Time as time columns", func() {
var nilPointer *time.Time
fixtures := make([]interface{}, 3)
fixtures := make([]interface{}, 5)
fixtures[0] = dt
fixtures[1] = &dt
fixtures[2] = nilPointer
fixtures[2] = earlyDt
fixtures[3] = &earlyDt
fixtures[4] = nilPointer
for i := range fixtures {
ConvertSqlTimeColumnToEpochMs(fixtures, i)
......@@ -27,9 +30,13 @@ func TestSqlEngine(t *testing.T) {
Convey("When converting them should return epoch time with millisecond precision ", func() {
expected := float64(dt.UnixNano()) / float64(time.Millisecond)
expectedEarly := float64(earlyDt.UnixNano()) / float64(time.Millisecond)
So(fixtures[0].(float64), ShouldEqual, expected)
So(fixtures[1].(float64), ShouldEqual, expected)
So(fixtures[2], ShouldBeNil)
So(fixtures[2].(float64), ShouldEqual, expectedEarly)
So(fixtures[3].(float64), ShouldEqual, expectedEarly)
So(fixtures[4], ShouldBeNil)
})
})
......
......@@ -70,6 +70,10 @@ export class AnnotationsEditorCtrl {
this.mode = 'list';
}
move(index, dir) {
_.move(this.annotations, index, index + dir);
}
add() {
this.annotations.push(this.currentAnnotation);
this.reset();
......
......@@ -21,7 +21,7 @@
</tr>
</thead>
<tbody>
<tr ng-repeat="annotation in ctrl.annotations">
<tr ng-repeat="annotation in ctrl.annotations track by annotation.name">
<td style="width:90%" ng-hide="annotation.builtIn" class="pointer" ng-click="ctrl.edit(annotation)">
<i class="fa fa-comment" style="color:{{annotation.iconColor}}"></i> &nbsp;
{{annotation.name}}
......@@ -33,8 +33,8 @@
<td class="pointer" ng-click="ctrl.edit(annotation)">
{{annotation.datasource || 'Default'}}
</td>
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
<td style="width: 1%"><i ng-click="ctrl.move($index,-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
<td style="width: 1%"><i ng-click="ctrl.move($index,1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
<td style="width: 1%">
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini" ng-hide="annotation.builtIn">
<i class="fa fa-remove"></i>
......
import React from 'react';
import ReactGridLayout from 'react-grid-layout-grafana';
import ReactGridLayout from 'react-grid-layout';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
import { DashboardPanel } from './DashboardPanel';
import { DashboardModel } from '../dashboard_model';
......
......@@ -48,7 +48,7 @@ export class SaveProvisionedDashboardModalCtrl {
constructor(dashboardSrv) {
this.dash = dashboardSrv.getCurrent().getSaveModelClone();
delete this.dash.id;
this.dashboardJson = JSON.stringify(this.dash, null, 2);
this.dashboardJson = angular.toJson(this.dash, true);
}
save() {
......
......@@ -2,6 +2,7 @@ import { coreModule, appEvents, contextSrv } from 'app/core/core';
import { DashboardModel } from '../dashboard_model';
import $ from 'jquery';
import _ from 'lodash';
import angular from 'angular';
import config from 'app/core/config';
export class SettingsCtrl {
......@@ -118,7 +119,7 @@ export class SettingsCtrl {
this.viewId = this.$location.search().editview;
if (this.viewId) {
this.json = JSON.stringify(this.dashboard.getSaveModelClone(), null, 2);
this.json = angular.toJson(this.dashboard.getSaveModelClone(), true);
}
if (this.viewId === 'settings' && this.dashboard.meta.canMakeEditable) {
......
......@@ -50,11 +50,11 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
data: params.join('&'),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Dashboard-Id': options.dashboardId, // enables distributed tracing in ds_proxy
'X-Panel-Id': options.panelId, // enables distributed tracing in ds_proxy
},
};
this.addTracingHeaders(httpOptions, options);
if (options.panelId) {
httpOptions.requestId = this.name + '.panelId.' + options.panelId;
}
......@@ -62,6 +62,14 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
};
this.addTracingHeaders = function(httpOptions, options) {
var proxyMode = !this.url.match(/^http/);
if (proxyMode) {
httpOptions.headers['X-Dashboard-Id'] = options.dashboardId;
httpOptions.headers['X-Panel-Id'] = options.panelId;
}
};
this.convertDataPointsToMs = function(result) {
if (!result || !result.data) {
return [];
......
......@@ -9,16 +9,18 @@ describe('graphiteDatasource', () => {
backendSrv: {},
$q: $q,
templateSrv: new TemplateSrvStub(),
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
};
beforeEach(function() {
ctx.instanceSettings = { url: [''], name: 'graphiteProd', jsonData: {} };
ctx.instanceSettings.url = '/api/datasources/proxy/1';
ctx.ds = new GraphiteDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv);
});
describe('When querying graphite with one target using query editor target spec', function() {
let query = {
panelId: 3,
dashboardId: 5,
rangeRaw: { from: 'now-1h', to: 'now' },
targets: [{ target: 'prod1.count' }, { target: 'prod2.count' }],
maxDataPoints: 500,
......@@ -40,8 +42,13 @@ describe('graphiteDatasource', () => {
});
});
it('X-Dashboard and X-Panel headers to be set!', () => {
expect(requestOptions.headers['X-Dashboard-Id']).toBe(5);
expect(requestOptions.headers['X-Panel-Id']).toBe(3);
});
it('should generate the correct query', function() {
expect(requestOptions.url).toBe('/render');
expect(requestOptions.url).toBe('/api/datasources/proxy/1/render');
});
it('should set unique requestId', function() {
......@@ -228,7 +235,7 @@ describe('graphiteDatasource', () => {
results = data;
});
expect(requestOptions.url).toBe('/tags/autoComplete/tags');
expect(requestOptions.url).toBe('/api/datasources/proxy/1/tags/autoComplete/tags');
expect(requestOptions.params.expr).toEqual([]);
expect(results).not.toBe(null);
});
......@@ -238,7 +245,7 @@ describe('graphiteDatasource', () => {
results = data;
});
expect(requestOptions.url).toBe('/tags/autoComplete/tags');
expect(requestOptions.url).toBe('/api/datasources/proxy/1/tags/autoComplete/tags');
expect(requestOptions.params.expr).toEqual(['server=backend_01']);
expect(results).not.toBe(null);
});
......@@ -248,7 +255,7 @@ describe('graphiteDatasource', () => {
results = data;
});
expect(requestOptions.url).toBe('/tags/autoComplete/tags');
expect(requestOptions.url).toBe('/api/datasources/proxy/1/tags/autoComplete/tags');
expect(requestOptions.params.expr).toEqual(['server=backend_01']);
expect(results).not.toBe(null);
});
......@@ -258,7 +265,7 @@ describe('graphiteDatasource', () => {
results = data;
});
expect(requestOptions.url).toBe('/tags/autoComplete/values');
expect(requestOptions.url).toBe('/api/datasources/proxy/1/tags/autoComplete/values');
expect(requestOptions.params.tag).toBe('server');
expect(requestOptions.params.expr).toEqual([]);
expect(results).not.toBe(null);
......@@ -269,7 +276,7 @@ describe('graphiteDatasource', () => {
results = data;
});
expect(requestOptions.url).toBe('/tags/autoComplete/values');
expect(requestOptions.url).toBe('/api/datasources/proxy/1/tags/autoComplete/values');
expect(requestOptions.params.tag).toBe('server');
expect(requestOptions.params.expr).toEqual(['server=~backend*']);
expect(results).not.toBe(null);
......@@ -280,7 +287,7 @@ describe('graphiteDatasource', () => {
results = data;
});
expect(requestOptions.url).toBe('/tags/autoComplete/values');
expect(requestOptions.url).toBe('/api/datasources/proxy/1/tags/autoComplete/values');
expect(requestOptions.params.tag).toBe('server');
expect(requestOptions.params.expr).toEqual([]);
expect(results).not.toBe(null);
......@@ -291,10 +298,46 @@ describe('graphiteDatasource', () => {
results = data;
});
expect(requestOptions.url).toBe('/tags/autoComplete/values');
expect(requestOptions.url).toBe('/api/datasources/proxy/1/tags/autoComplete/values');
expect(requestOptions.params.tag).toBe('server');
expect(requestOptions.params.expr).toEqual(['server=~backend*']);
expect(results).not.toBe(null);
});
});
});
function accessScenario(name, url, fn) {
describe('access scenario ' + name, function() {
let ctx: any = {
backendSrv: {},
$q: $q,
templateSrv: new TemplateSrvStub(),
instanceSettings: { url: 'url', name: 'graphiteProd', jsonData: {} },
};
let httpOptions = {
headers: {},
};
describe('when using proxy mode', () => {
let options = { dashboardId: 1, panelId: 2 };
it('tracing headers should be added', () => {
ctx.instanceSettings.url = url;
var ds = new GraphiteDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv);
ds.addTracingHeaders(httpOptions, options);
fn(httpOptions);
});
});
});
}
accessScenario('with proxy access', '/api/datasources/proxy/1', function(httpOptions) {
expect(httpOptions.headers['X-Dashboard-Id']).toBe(1);
expect(httpOptions.headers['X-Panel-Id']).toBe(2);
});
accessScenario('with direct access', 'http://localhost:8080', function(httpOptions) {
expect(httpOptions.headers['X-Dashboard-Id']).toBe(undefined);
expect(httpOptions.headers['X-Panel-Id']).toBe(undefined);
});
<?xml version="1.0" standalone="no"?>
<!-- Generator: Adobe Fireworks CS6, Export SVG Extension by Aaron Beall (http://fireworks.abeall.com) . Version: 0.6.1 -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg id="Untitled-Page%201" viewBox="0 0 6 6" style="background-color:#ffffff00" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"
x="0px" y="0px" width="6px" height="6px"
>
<g opacity="0.302">
<path d="M 6 6 L 0 6 L 0 4.2 L 4 4.2 L 4.2 4.2 L 4.2 0 L 6 0 L 6 6 L 6 6 Z" fill="#FFFFFF"/>
</g>
</svg>
@import '~react-grid-layout-grafana/css/styles.css';
@import '~react-grid-layout/css/styles.css';
@import '~react-resizable/css/styles.css';
.panel-in-fullscreen {
......@@ -44,11 +44,6 @@
border-right: 2px solid $gray-1;
border-bottom: 2px solid $gray-1;
}
// temp fix since we use old commit of grid component
// this can be removed when we revert to non fork grid component
.react-grid-item > .react-resizable-handle {
background-image: url('../img/resize-handle-white.svg');
}
}
.theme-light {
......
......@@ -7,13 +7,14 @@
.singlestat-panel-value-container {
line-height: 1;
display: table-cell;
vertical-align: middle;
text-align: center;
position: relative;
position: absolute;
z-index: 1;
font-size: 3em;
font-weight: bold;
margin: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.singlestat-panel-prefix {
......
......@@ -26,6 +26,8 @@ module.exports = merge(common, {
extensions: ['.scss', '.ts', '.tsx', '.es6', '.js', '.json', '.svg', '.woff2', '.png'],
},
devtool: 'eval-source-map',
devServer: {
publicPath: '/public/build/',
hot: true,
......
......@@ -405,17 +405,17 @@ angular-native-dragdrop@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/angular-native-dragdrop/-/angular-native-dragdrop-1.2.2.tgz#d646c6b75b131c48073c3f6e36a225b2726d8bae"
angular-route@^1.6.6:
version "1.6.10"
resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.10.tgz#4247a32eab19495624623e96c1626dfba17ebf21"
angular-route@1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/angular-route/-/angular-route-1.6.6.tgz#8c11748aa195c717b1b615a7e746442bfc7c61f4"
angular-sanitize@^1.6.6:
version "1.6.10"
resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.10.tgz#635a362afb2dd040179f17d3a5455962b2c1918f"
angular-sanitize@1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.6.tgz#0fd065a19931517fbece66596d325d72b6e06041"
angular@^1.6.6:
version "1.6.10"
resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.10.tgz#eed3080a34d29d0f681ff119b18ce294e3f74826"
angular@1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/angular/-/angular-1.6.6.tgz#fd5a3cfb437ce382d854ee01120797978527cb64"
ansi-align@^2.0.0:
version "2.0.0"
......@@ -8898,22 +8898,22 @@ react-dom@^16.2.0:
object-assign "^4.1.1"
prop-types "^15.6.0"
"react-draggable@^2.2.6 || ^3.0.3", react-draggable@^3.0.3:
react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3":
version "3.0.5"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.5.tgz#c031e0ed4313531f9409d6cd84c8ebcec0ddfe2d"
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react-grid-layout-grafana@0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/react-grid-layout-grafana/-/react-grid-layout-grafana-0.16.0.tgz#12242153fcd0bb80a26af8e41694bc2fde788b3a"
react-grid-layout@0.16.6:
version "0.16.6"
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.6.tgz#9b2407a2b946c2260ebaf66f13b556e1da4efeb2"
dependencies:
classnames "2.x"
lodash.isequal "^4.0.0"
prop-types "15.x"
react-draggable "^3.0.3"
react-resizable "^1.7.5"
react-draggable "3.x"
react-resizable "1.x"
react-highlight-words@^0.10.0:
version "0.10.0"
......@@ -8973,7 +8973,7 @@ react-reconciler@^0.7.0:
object-assign "^4.1.1"
prop-types "^15.6.0"
react-resizable@^1.7.5:
react-resizable@1.x:
version "1.7.5"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
dependencies:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment