Commit acff78d4 by Torkel Ödegaard

Merge branch 'master' into heatmap-refactoring2

parents fbf39598 8bbff2c4
......@@ -10,7 +10,6 @@
* **Prometheus**: Support table response formats (column per label) [#6140](https://github.com/grafana/grafana/issues/6140), thx [@mtanda](https://github.com/mtanda)
* **Single Stat Panel**: support for non time series data [#6564](https://github.com/grafana/grafana/issues/6564)
## Minor Enchancements
* **Prometheus**: Make Prometheus query field a textarea [#7663](https://github.com/grafana/grafana/issues/7663), thx [@hagen1778](https://github.com/hagen1778)
......@@ -22,12 +21,16 @@
* **Templating**: Data source variable now supports multi value and panel repeats [#7030](https://github.com/grafana/grafana/issues/7030) thx [@mtanda](https://github.com/mtanda)
* **Telegram**: Telegram alert is not sending metric and legend. [#8110](https://github.com/grafana/grafana/issues/8110), thx [@bashgeek](https://github.com/bashgeek)
* **Graph**: Support dashed lines [#514](https://github.com/grafana/grafana/issues/514), thx [@smalik03](https://github.com/smalik03)
* **Table**: Support to change column header text [#3551](https://github.com/grafana/grafana/issues/3551)
## Fixes
* **Table Panel**: Fixed annotation display in table panel, [#8023](https://github.com/grafana/grafana/issues/8023)
* **Dashboard**: If refresh is blocked due to tab not visible, then refresh when it becomes visible [#8076](https://github.com/grafana/grafana/issues/8076) thanks [@SimenB](https://github.com/SimenB)
* **Snapshots**: Fixed problem with annotations & snapshots [#7659](https://github.com/grafana/grafana/issues/7659)
## Changes
* **Elasticsearch**: Changed elasticsearch Terms aggregation to default to Min Doc Count to 1, and sort order to Top [#8321](https://github.com/grafana/grafana/issues/8321)
# 4.2.0 (2017-03-22)
## Minor Enhancements
* **Templates**: Prevent use of the prefix `__` for templates in web UI [#7678](https://github.com/grafana/grafana/issues/7678)
......
......@@ -13,29 +13,26 @@ weight = 10
# Using AWS CloudWatch in Grafana
Grafana ships with built in support for CloudWatch. You just have to add it as a data source and you will
be ready to build dashboards for you CloudWatch metrics.
Grafana ships with built in support for CloudWatch. You just have to add it as a data source and you will be ready to build dashboards for you CloudWatch metrics.
## Adding the data source
![](/img/docs/cloudwatch/cloudwatch_add.png)
## Adding the data source to Grafana
1. Open the side menu by clicking the the Grafana icon in the top header.
1. Open the side menu by clicking the Grafana icon in the top header.
2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
3. Click the `+ Add data source` button in the top header.
4. Select `Cloudwatch` from the *Type* dropdown.
> NOTE: If this link is missing in the side menu it means that your current user does not have the `Admin` role for the current organization.
3. Click the `Add new` link in the top header.
4. Select `CloudWatch` from the dropdown.
> NOTE: If at any moment you have issues with getting this datasource to work and grafana is giving you undescriptive errors then dont forget to check your log file (try looking in /var/log/grafana/).
> NOTE: If at any moment you have issues with getting this datasource to work and Grafana is giving you undescriptive errors then don't
forget to check your log file (try looking in /var/log/grafana/grafana.log).
Name | Description
------------ | -------------
Name | The data source name, important that this is the same as in Grafana v1.x if you plan to import old dashboards.
Default | Default data source means that it will be pre-selected for new panels.
Credentials profile name | Specify the name of the profile to use (if you use `~/aws/credentials` file), leave blank for default. This option was introduced in Grafana 2.5.1
Default Region | Used in query editor to set region (can be changed on per query basis)
Custom Metrics namespace | Specify the CloudWatch namespace of Custom metrics
Assume Role Arn | Specify the ARN of the role to assume
*Name* | The data source name. This is how you refer to the data source in panels & queries.
*Default* | Default data source means that it will be pre-selected for new panels.
*Credentials* profile name | Specify the name of the profile to use (if you use `~/aws/credentials` file), leave blank for default.
*Default Region* | Used in query editor to set region (can be changed on per query basis)
*Custom Metrics namespace* | Specify the CloudWatch namespace of Custom metrics
*Assume Role Arn* | Specify the ARN of the role to assume
## Authentication
......@@ -61,49 +58,64 @@ Example content:
## Metric Query Editor
![](/img/docs/cloudwatch/query_editor.png)
![](/img/docs/v43/cloudwatch_editor.png)
You need to specify a namespace, metric, at least one stat, and at least one dimension.
## Templated queries
CloudWatch Datasource Plugin provides the following functions in `Variables values query` field in Templating Editor to query `region`, `namespaces`, `metric names` and `dimension keys/values` on the CloudWatch.
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.
Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns makes it easy to change the data
being displayed in your dashboard.
Checkout the [Templating]({{< relref "reference/templating.md" >}}) documentation for an introduction to the templating feature and the different
types of template variables.
### Query variable
CloudWatch Datasource Plugin provides the following queries you can specify in the `Query` field in the Variable
edit view. They allow you to fill a variable's options list with things like `region`, `namespaces`, `metric names`
and `dimension keys/values`.
Name | Description
------- | --------
`regions()` | Returns a list of regions AWS provides their service.
`namespaces()` | Returns a list of namespaces CloudWatch support.
`metrics(namespace, [region])` | Returns a list of metrics in the namespace. (specify region for custom metrics)
`dimension_keys(namespace)` | Returns a list of dimension keys in the namespace.
`dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`.
`ebs_volume_ids(region, instance_id)` | Returns a list of volume id matching the specified `region`, `instance_id`.
`ec2_instance_attribute(region, attribute_name, filters)` | Returns a list of attribute matching the specified `region`, `attribute_name`, `filters`.
*regions()* | Returns a list of regions AWS provides their service.
*namespaces()* | Returns a list of namespaces CloudWatch support.
*metrics(namespace, [region])* | Returns a list of metrics in the namespace. (specify region for custom metrics)
*dimension_keys(namespace)* | Returns a list of dimension keys in the namespace.
*dimension_values(region, namespace, metric, dimension_key)* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`.
*ebs_volume_ids(region, instance_id)* | Returns a list of volume id matching the specified `region`, `instance_id`.
*ec2_instance_attribute(region, attribute_name, filters)* | Returns a list of attribute matching the specified `region`, `attribute_name`, `filters`.
For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
## Example templated Queries
#### Examples templated Queries
Example dimension queries which will return list of resources for individual AWS Services:
Service | Query
Query | Service
------- | -----
ELB | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)`
RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)`
RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)`
S3 | `dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)`
*dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)* | ELB
*dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)* | ElastiCache
*dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)* | RedShift
*dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS
*dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)* | S3
## ec2_instance_attribute JSON filters
#### ec2_instance_attribute JSON filters
The `ec2_instance_attribute` query take `filters` in JSON format.
You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html).
Specify like `{ filter_name1: [ filter_value1 ], filter_name2: [ filter_value2 ] }`
Filters syntax:
```javascript
{ filter_name1: [ filter_value1 ], filter_name2: [ filter_value2 ] }
```
Example `ec2_instance_attribute()` query
ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] })
![](/img/docs/v2/cloudwatch_templating.png)
## Cost
Amazon provides 1 million CloudWatch API requests each month at no additional charge. Past this,
......
......@@ -85,8 +85,9 @@ The column styles allow you control how dates and numbers are formatted.
1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values.
2. `Type`: The three supported types of types are `Number`, `String` and `Date`.
3. `Format`: Specify date format. Only available when `Type` is set to `Date`.
4. `Coloring` and `Thresholds`: Specify color mode and thresholds limits.
5. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.
6. `Add column style rule`: Add new column rule.
3. `Title`: Title for the column, when using a Regex the title can include replacement strings like `$1`.
4. `Format`: Specify date format. Only available when `Type` is set to `Date`.
5. `Coloring` and `Thresholds`: Specify color mode and thresholds limits.
6. `Unit` and `Decimals`: Specify unit and decimal precision for numbers.
7. `Add column style rule`: Add new column rule.
......@@ -19,26 +19,28 @@ Creates a new dashboard or updates an existing dashboard.
**Example Request for new dashboard**:
POST /api/dashboards/db HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"dashboard": {
"id": null,
"title": "Production Overview",
"tags": [ "templated" ],
"timezone": "browser",
"rows": [
{
}
],
"schemaVersion": 6,
"version": 0
},
"overwrite": false
}
```http
POST /api/dashboards/db HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"dashboard": {
"id": null,
"title": "Production Overview",
"tags": [ "templated" ],
"timezone": "browser",
"rows": [
{
}
],
"schemaVersion": 6,
"version": 0
},
"overwrite": false
}
```
JSON Body schema:
......@@ -47,15 +49,17 @@ JSON Body schema:
**Example Response**:
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 78
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 78
{
"slug": "production-overview",
"status": "success",
"version": 1
}
{
"slug": "production-overview",
"status": "success",
"version": 1
}
```
Status Codes:
......@@ -67,14 +71,16 @@ Status Codes:
The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the version that was sent). The
same status code is also used if another dashboard exists with the same title. The response body will look like this:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json; charset=UTF-8
Content-Length: 97
```http
HTTP/1.1 412 Precondition Failed
Content-Type: application/json; charset=UTF-8
Content-Length: 97
{
"message": "The dashboard has been changed by someone else",
"status": "version-mismatch"
}
{
"message": "The dashboard has been changed by someone else",
"status": "version-mismatch"
}
```
In in case of title already exists the `status` property will be `name-exists`.
......@@ -86,34 +92,38 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver
**Example Request**:
GET /api/dashboards/db/production-overview HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```http
GET /api/dashboards/db/production-overview HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{
"meta": {
"isStarred": false,
"slug": "production-overview"
},
"dashboard": {
"id": null,
"title": "Production Overview",
"tags": [ "templated" ],
"timezone": "browser",
"rows": [
{
}
],
"schemaVersion": 6,
"version": 0
```http
HTTP/1.1 200
Content-Type: application/json
{
"meta": {
"isStarred": false,
"slug": "production-overview"
},
"dashboard": {
"id": null,
"title": "Production Overview",
"tags": [ "templated" ],
"timezone": "browser",
"rows": [
{
}
}
],
"schemaVersion": 6,
"version": 0
}
}
```
## Delete dashboard
......@@ -123,17 +133,21 @@ The above will delete the dashboard with the specified slug. The slug is the url
**Example Request**:
DELETE /api/dashboards/db/test HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```http
DELETE /api/dashboards/db/test HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
```http
HTTP/1.1 200
Content-Type: application/json
{"title": "Test"}
{"title": "Test"}
```
## Gets the home dashboard
......
......@@ -22,10 +22,10 @@ installation.
## Install Stable
```
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.2.0_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_4.2.0_amd64.deb
```bash
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.2.0_amd64.deb
sudo apt-get install -y adduser libfontconfig
sudo dpkg -i grafana_4.2.0_amd64.deb
```
## APT Repository
......@@ -43,18 +43,24 @@ candidates.
Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
allows you to install signed packages.
$ curl https://packagecloud.io/gpg.key | sudo apt-key add -
```bash
curl https://packagecloud.io/gpg.key | sudo apt-key add -
```
Update your Apt repositories and install Grafana
$ sudo apt-get update
$ sudo apt-get install grafana
```bash
sudo apt-get update
sudo apt-get install grafana
```
On some older versions of Ubuntu and Debian you may need to install the
`apt-transport-https` package which is needed to fetch packages over
HTTPS.
$ sudo apt-get install -y apt-transport-https
```bash
sudo apt-get install -y apt-transport-https
```
## Package details
......@@ -70,7 +76,9 @@ HTTPS.
Start Grafana by running:
$ sudo service grafana-server start
```bash
sudo service grafana-server start
```
This will start the `grafana-server` process as the `grafana` user,
which was created during the package installation. The default HTTP port
......@@ -78,19 +86,25 @@ is `3000` and default user and group is `admin`.
To configure the Grafana server to start at boot time:
$ sudo update-rc.d grafana-server defaults
```bash
sudo update-rc.d grafana-server defaults
```
## Start the server (via systemd)
To start the service using systemd:
$ systemctl daemon-reload
$ systemctl start grafana-server
$ systemctl status grafana-server
```bash
systemctl daemon-reload
systemctl start grafana-server
systemctl status grafana-server
```
Enable the systemd service so that Grafana starts at boot.
sudo systemctl enable grafana-server.service
```bash
sudo systemctl enable grafana-server.service
```
## Environment file
......
......@@ -77,7 +77,7 @@ function (_, $, coreModule) {
$scope.source = function(query, callback) {
$scope.$apply(function() {
$scope.getOptions({ measurementFilter: query }).then(function(altSegments) {
$scope.getOptions({ $query: query }).then(function(altSegments) {
$scope.altSegments = altSegments;
options = _.map($scope.altSegments, function(alt) { return alt.value; });
......
......@@ -61,7 +61,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
attrs: {dashboard: "ctrl.dashboard", panel: "panel", row: "ctrl.row"},
};
var panelElemName = 'panel-' + scope.panel.type;
let panelInfo = config.panels[scope.panel.type];
var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
if (panelInfo) {
......
......@@ -53,7 +53,7 @@ export function exportTableDataToCsv(table) {
var text = 'sep=;\n';
// add header
_.each(table.columns, function(column) {
text += column.text + ';';
text += (column.title || column.text) + ';';
});
text += '\n';
// process data
......
......@@ -81,9 +81,9 @@ function (angular, _, queryDef) {
switch($scope.agg.type) {
case 'terms': {
settings.order = settings.order || "asc";
settings.order = settings.order || "desc";
settings.size = settings.size || "10";
settings.min_doc_count = settings.min_doc_count || 0;
settings.min_doc_count = settings.min_doc_count || 1;
settings.orderBy = settings.orderBy || "_term";
if (settings.size !== '0') {
......
......@@ -119,7 +119,7 @@ function (queryDef) {
}
query.script_fields = {},
query.fielddata_fields = [this.timeField];
query.docvalue_fields = [this.timeField];
return query;
};
......
......@@ -11,7 +11,7 @@
<label class="gf-form-label query-keyword width-7">FROM</label>
<metric-segment segment="ctrl.policySegment" get-options="ctrl.getPolicySegments()" on-change="ctrl.policyChanged()"></metric-segment>
<metric-segment segment="ctrl.measurementSegment" get-options="ctrl.getMeasurements(measurementFilter)" on-change="ctrl.measurementChanged()"></metric-segment>
<metric-segment segment="ctrl.measurementSegment" get-options="ctrl.getMeasurements($query)" on-change="ctrl.measurementChanged()"></metric-segment>
</div>
<div class="gf-form">
......
......@@ -81,8 +81,8 @@ describe('grafanaHeatmap', function () {
getTimezone: sinon.stub().returns('utc')
},
range: {
from: moment.utc("01 Mar 2017 10:00:00"),
to: moment.utc("01 Mar 2017 11:00:00"),
from: moment.utc("01 Mar 2017 10:00:00", 'DD MMM YYYY HH:mm:ss'),
to: moment.utc("01 Mar 2017 11:00:00", 'DD MMM YYYY HH:mm:ss'),
},
};
......@@ -263,5 +263,5 @@ function getTicks(element, axisSelector) {
function formatLocalTime(timeStr) {
let format = "HH:mm";
return moment.utc(timeStr).local().format(format);
return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').local().format(format);
}
<div class="edit-tab-with-sidemenu">
<aside class="edit-sidemenu-aside">
<ul class="edit-sidemenu">
<li ng-repeat="style in editor.panel.styles" ng-class="{active: editor.activeStyleIndex === $index}">
<a ng-click="editor.activeStyleIndex = $index" >{{style.pattern || 'New rule'}}</a>
</li>
<li>
<a class="pointer" ng-click="editor.addColumnStyle()">
<i class="fa fa-plus"></i>&nbsp;Add
</a>
</li>
</ul>
</aside>
<div class="edit-tab-content" ng-repeat="style in editor.panel.styles" ng-if="editor.activeStyleIndex === $index">
<div class="section gf-form-group">
<h5 class="section-heading">Options</h5>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-13">Apply to columns named</label>
<input type="text" placeholder="Name or regex" class="gf-form-input width-13" ng-model="style.pattern" bs-tooltip="'Specify regex using /my.*regex/ syntax'" bs-typeahead="editor.getColumnNames" ng-blur="editor.render()" data-min-length=0 data-items=100 ng-model-onblur data-placement="right">
</div>
</div>
<div class="gf-form" ng-if="style.type !== 'hidden'">
<label class="gf-form-label width-13">Column Header</label>
<input type="text" class="gf-form-input width-13" ng-model="style.alias" ng-change="editor.render()" ng-model-onblur placeholder="Override header label">
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Type</h5>
<div class="gf-form">
<label class="gf-form-label width-8">Type</label>
<div class="gf-form-select-wrapper width-10">
<select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form" ng-if="style.type === 'date'">
<label class="gf-form-label width-8">Date Format</label>
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
</div>
<div ng-if="style.type === 'string'">
<gf-form-switch class="gf-form" label-class="width-8" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize" change="editor.render()"></gf-form-switch>
</div>
<div ng-if="style.type === 'number'">
<div class="gf-form">
<label class="gf-form-label width-8">Unit</label>
<div class="gf-form-dropdown-typeahead width-10" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Decimals</label>
<input type="number" class="gf-form-input width-4" data-placement="right" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
</div>
</div>
</div>
<div class="section gf-form-group" ng-if="style.type === 'number'">
<h5 class="section-heading">Thresholds</h5>
<div class="gf-form">
<label class="gf-form-label width-8">Thresholds<tip>Comma separated values</tip></label>
<input type="text" class="gf-form-input width-10" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()" array-join ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Color Mode</label>
<div class="gf-form-select-wrapper width-10">
<select class="gf-form-input" ng-model="style.colorMode" ng-options="c.value as c.text for c in editor.colorModes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Colors</label>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[0]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[1]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[2]" ng-change="editor.render()"></spectrum-picker>
</span>
<div class="gf-form-label">
<a class="pointer" ng-click="editor.invertColorOrder($index)">Invert</a>
</div>
</div>
</div>
<div class="clearfix"></div>
<button class="btn btn-danger btn-small" ng-click="editor.removeColumnStyle(style)">
<i class="fa fa-trash"></i> Remove Rule
</button>
<br />
<br />
</div>
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment';
import angular from 'angular';
import kbn from 'app/core/utils/kbn';
export class ColumnOptionsCtrl {
panel: any;
panelCtrl: any;
colorModes: any;
columnStyles: any;
columnTypes: any;
fontSizes: any;
dateFormats: any;
addColumnSegment: any;
unitFormats: any;
getColumnNames: any;
activeStyleIndex: number;
/** @ngInject */
constructor($scope, private $q, private uiSegmentSrv) {
$scope.editor = this;
this.activeStyleIndex = 0;
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
this.unitFormats = kbn.getUnitFormats();
this.colorModes = [
{text: 'Disabled', value: null},
{text: 'Cell', value: 'cell'},
{text: 'Value', value: 'value'},
{text: 'Row', value: 'row'},
];
this.columnTypes = [
{text: 'Number', value: 'number'},
{text: 'String', value: 'string'},
{text: 'Date', value: 'date'},
{text: 'Hidden', value: 'hidden'}
];
this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
this.dateFormats = [
{text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
{text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
{text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
];
this.getColumnNames = () => {
if (!this.panelCtrl.table) {
return [];
}
return _.map(this.panelCtrl.table.columns, function(col: any) {
return col.text;
});
};
}
render() {
this.panelCtrl.render();
}
setUnitFormat(column, subItem) {
column.unit = subItem.value;
this.panelCtrl.render();
}
addColumnStyle() {
var newStyleRule = {
unit: 'short',
type: 'number',
alias: '',
decimals: 2,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
colorMode: null,
pattern: '',
dateFormat: 'YYYY-MM-DD HH:mm:ss',
thresholds: [],
};
var styles = this.panel.styles;
var stylesCount = styles.length;
var indexToInsert = stylesCount;
// check if last is a catch all rule, then add it before that one
if (stylesCount > 0) {
var last = styles[stylesCount-1];
if (last.pattern === '/.*/') {
indexToInsert = stylesCount-1;
}
}
styles.splice(indexToInsert, 0, newStyleRule);
this.activeStyleIndex = indexToInsert;
}
removeColumnStyle(style) {
this.panel.styles = _.without(this.panel.styles, style);
}
invertColorOrder(index) {
var ref = this.panel.styles[index].colors;
var copy = ref[0];
ref[0] = ref[2];
ref[2] = copy;
this.panelCtrl.render();
}
}
/** @ngInject */
export function columnOptionsTab($q, uiSegmentSrv) {
'use strict';
return {
restrict: 'E',
scope: true,
templateUrl: 'public/app/plugins/panel/table/column_options.html',
controller: ColumnOptionsCtrl,
};
}
......@@ -21,120 +21,27 @@
<metric-segment segment="editor.addColumnSegment" get-options="editor.getColumnOptions()" on-change="editor.addColumn()"></metric-segment>
</div>
</div>
<gf-form-switch class="gf-form" label-class="width-10"
label="Filter null values"
checked="editor.panel.filterNull"
on-change="editor.render()"></gf-form-switch>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Table Display</h5>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-8">Rows per page</label>
<input type="number" class="gf-form-input width-6"
placeholder="100" data-placement="right"
ng-model="editor.panel.pageSize"
ng-change="editor.render()"
ng-model-onblur>
</div>
<gf-form-switch class="gf-form" label-class="width-4" label="Scroll" checked="editor.panel.scroll" on-change="editor.render()"></gf-form-switch>
<div class="gf-form max-width-17">
<label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input"
ng-model="editor.panel.fontSize"
ng-options="f for f in editor.fontSizes"
ng-change="editor.render()"></select>
</div>
</div>
</div>
</div>
</div>
<div class="editor-row">
<div class="section gf-form-group">
<h5 class="section-heading">Column Styles</h5>
<div ng-repeat="style in editor.panel.styles">
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label">Name or regex</label>
<input type="text" class="gf-form-input" ng-model="style.pattern" bs-typeahead="editor.getColumnNames" ng-blur="editor.render()" data-min-length=0 data-items=100 ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label">Type</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form" ng-if="style.type === 'date'">
<label class="gf-form-label">Format</label>
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
</div>
<gf-form-switch class="gf-form" label-class="width-8" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize" change="editor.render()"></gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" ng-click="editor.removeColumnStyle(style)">
<i class="fa fa-trash"></i>
</a>
</label>
</div>
</div>
<div class="gf-form-inline" ng-if="style.type === 'number'">
<div class="gf-form offset-width-8">
<label class="gf-form-label width-8">Unit</label>
</div>
<div class="gf-form">
<div class="gf-form-dropdown-typeahead" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
</div>
<div class="gf-form">
<label class="gf-form-label">Decimals</label>
<input type="number" class="gf-form-input width-4" data-placement="right" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label">Coloring</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input" ng-model="style.colorMode" ng-options="c.value as c.text for c in editor.colorModes" ng-change="editor.render()"></select>
</div>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<div class="gf-form-inline" ng-if="style.type === 'number'">
<div class="gf-form offset-width-8">
<label class="gf-form-label width-8">Thresholds<tip>Comma separated values</tip></label>
<input type="text" class="gf-form-input width-10" ng-model="style.thresholds" placeholder="50,80" ng-blur="editor.render()" array-join ng-model-onblur>
</div>
<div class="gf-form">
<label class="gf-form-label width-5">Colors</label>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[0]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[1]" ng-change="editor.render()"></spectrum-picker>
</span>
<span class="gf-form-label">
<spectrum-picker ng-model="style.colors[2]" ng-change="editor.render()"></spectrum-picker>
</span>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow">
<a class="pointer" ng-click="editor.invertColorOrder($index)">Invert</a>
</div>
</div>
<h5 class="section-heading">Paging</h5>
<div class="gf-form">
<label class="gf-form-label width-8">Rows per page</label>
<input type="number" class="gf-form-input width-6"
placeholder="100" data-placement="right"
ng-model="editor.panel.pageSize"
ng-change="editor.render()"
ng-model-onblur>
</div>
<gf-form-switch class="gf-form" label-class="width-8" switch-class="max-width-6" label="Scroll" checked="editor.panel.scroll" on-change="editor.render()"></gf-form-switch>
<div class="gf-form max-width-17">
<label class="gf-form-label width-8">Font size</label>
<div class="gf-form-select-wrapper width-6">
<select class="gf-form-input"
ng-model="editor.panel.fontSize"
ng-options="f for f in editor.fontSizes"
ng-change="editor.render()"></select>
</div>
</div>
</div>
<div class="gf-form-button">
<button class="btn btn-inverse" ng-click="editor.addColumnStyle()">
<i class="fa fa-plus"></i>&nbsp;Add column style rule
</button>
</div>
</div>
......@@ -13,13 +13,8 @@ export class TablePanelEditorCtrl {
panel: any;
panelCtrl: any;
transformers: any;
colorModes: any;
columnStyles: any;
columnTypes: any;
fontSizes: any;
dateFormats: any;
addColumnSegment: any;
unitFormats: any;
getColumnNames: any;
/** @ngInject */
......@@ -28,37 +23,9 @@ export class TablePanelEditorCtrl {
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
this.transformers = transformers;
this.unitFormats = kbn.getUnitFormats();
this.colorModes = [
{text: 'Disabled', value: null},
{text: 'Cell', value: 'cell'},
{text: 'Value', value: 'value'},
{text: 'Row', value: 'row'},
];
this.columnTypes = [
{text: 'Number', value: 'number'},
{text: 'String', value: 'string'},
{text: 'Date', value: 'date'},
{text: 'Hidden', value: 'hidden'}
];
this.fontSizes = ['80%', '90%', '100%', '110%', '120%', '130%', '150%', '160%', '180%', '200%', '220%', '250%'];
this.dateFormats = [
{text: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss'},
{text: 'MM/DD/YY h:mm:ss a', value: 'MM/DD/YY h:mm:ss a'},
{text: 'MMMM D, YYYY LT', value: 'MMMM D, YYYY LT'},
];
this.addColumnSegment = uiSegmentSrv.newPlusButton();
// this is used from bs-typeahead and needs to be instance bound
this.getColumnNames = () => {
if (!this.panelCtrl.table) {
return [];
}
return _.map(this.panelCtrl.table.columns, function(col: any) {
return col.text;
});
};
}
getColumnOptions() {
......@@ -97,38 +64,6 @@ export class TablePanelEditorCtrl {
this.panel.columns = _.without(this.panel.columns, column);
this.panelCtrl.render();
}
setUnitFormat(column, subItem) {
column.unit = subItem.value;
this.panelCtrl.render();
}
addColumnStyle() {
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: '/.*/',
dateFormat: 'YYYY-MM-DD HH:mm:ss',
thresholds: [],
};
this.panel.styles.push(angular.copy(columnStyleDefaults));
}
removeColumnStyle(style) {
this.panel.styles = _.without(this.panel.styles, style);
}
invertColorOrder(index) {
var ref = this.panel.styles[index].colors;
var copy = ref[0];
ref[0] = ref[2];
ref[2] = copy;
this.panelCtrl.render();
}
}
/** @ngInject */
......
......@@ -7,7 +7,7 @@
<tr>
<th ng-repeat="col in ctrl.table.columns" ng-hide="col.hidden">
<div class="table-panel-table-header-inner pointer" ng-click="ctrl.toggleColumnSort(col, $index)">
{{col.text}}
{{col.title}}
<span class="table-panel-table-header-controls" ng-if="col.sort">
<i class="fa fa-caret-down" ng-show="col.desc"></i>
<i class="fa fa-caret-up" ng-hide="col.desc"></i>
......
......@@ -8,6 +8,7 @@ import * as FileExport from 'app/core/utils/file_export';
import {MetricsPanelCtrl} from 'app/plugins/sdk';
import {transformDataToTable} from './transformers';
import {tablePanelEditor} from './editor';
import {columnOptionsTab} from './column_options';
import {TableRenderer} from './renderer';
class TablePanelCtrl extends MetricsPanelCtrl {
......@@ -16,6 +17,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
pageIndex: number;
dataRaw: any;
table: any;
renderer: any;
panelDefaults = {
targets: [{}],
......@@ -26,11 +28,13 @@ class TablePanelCtrl extends MetricsPanelCtrl {
{
type: 'date',
pattern: 'Time',
alias: 'Time',
dateFormat: 'YYYY-MM-DD HH:mm:ss',
},
{
unit: 'short',
type: 'number',
alias: '',
decimals: 2,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
colorMode: null,
......@@ -42,7 +46,6 @@ class TablePanelCtrl extends MetricsPanelCtrl {
scroll: true,
fontSize: '100%',
sort: {col: 0, desc: true},
filterNull: false,
};
/** @ngInject */
......@@ -68,6 +71,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
onInitEditMode() {
this.addEditorTab('Options', tablePanelEditor, 2);
this.addEditorTab('Column Styles', columnOptionsTab, 3);
}
onInitPanelActions(actions) {
......@@ -118,6 +122,9 @@ class TablePanelCtrl extends MetricsPanelCtrl {
render() {
this.table = transformDataToTable(this.dataRaw, this.panel);
this.table.sort(this.panel.sort);
this.renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize);
return super.render(this.table);
}
......@@ -141,8 +148,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
}
exportCsv() {
var renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize);
FileExport.exportTableDataToCsv(renderer.render_values());
FileExport.exportTableDataToCsv(this.renderer.render_values());
}
link(scope, elem, attrs, ctrl) {
......@@ -162,9 +168,9 @@ class TablePanelCtrl extends MetricsPanelCtrl {
}
function appendTableRows(tbodyElem) {
var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc(), ctrl.$sanitize);
ctrl.renderer.setTable(data);
tbodyElem.empty();
tbodyElem.html(renderer.render(ctrl.pageIndex));
tbodyElem.html(ctrl.renderer.render(ctrl.pageIndex));
}
function switchPage(e) {
......
......@@ -5,12 +5,44 @@ import moment from 'moment';
import kbn from 'app/core/utils/kbn';
export class TableRenderer {
formaters: any[];
formatters: any[];
colorState: any;
constructor(private panel, private table, private isUtc, private sanitize) {
this.formaters = [];
this.initColumns();
}
setTable(table) {
this.table = table;
this.initColumns();
}
initColumns() {
this.formatters = [];
this.colorState = {};
for (let colIndex = 0; colIndex < this.table.columns.length; colIndex++) {
let column = this.table.columns[colIndex];
column.title = column.text;
for (let i = 0; i < this.panel.styles.length; i++) {
let style = this.panel.styles[i];
var regex = kbn.stringToJsRegex(style.pattern);
if (column.text.match(regex)) {
column.style = style;
if (style.alias) {
column.title = column.text.replace(regex, style.alias);
}
break;
}
}
this.formatters[colIndex] = this.createColumnFormatter(column);
}
}
getColorForValue(value, style) {
......@@ -24,7 +56,7 @@ export class TableRenderer {
return _.first(style.colors);
}
defaultCellFormater(v, style) {
defaultCellFormatter(v, style) {
if (v === null || v === void 0 || v === undefined) {
return '';
}
......@@ -40,18 +72,18 @@ export class TableRenderer {
}
}
createColumnFormater(style, column) {
if (!style) {
return this.defaultCellFormater;
createColumnFormatter(column) {
if (!column.style) {
return this.defaultCellFormatter;
}
if (style.type === 'hidden') {
if (column.style.type === 'hidden') {
return v => {
return undefined;
};
}
if (style.type === 'date') {
if (column.style.type === 'date') {
return v => {
if (v === undefined || v === null) {
return '-';
......@@ -62,12 +94,12 @@ export class TableRenderer {
if (this.isUtc) {
date = date.utc();
}
return date.format(style.dateFormat);
return date.format(column.style.dateFormat);
};
}
if (style.type === 'number') {
let valueFormater = kbn.valueFormats[column.unit || style.unit];
if (column.style.type === 'number') {
let valueFormatter = kbn.valueFormats[column.unit || column.style.unit];
return v => {
if (v === null || v === void 0) {
......@@ -75,39 +107,24 @@ export class TableRenderer {
}
if (_.isString(v)) {
return this.defaultCellFormater(v, style);
return this.defaultCellFormatter(v, column.style);
}
if (style.colorMode) {
this.colorState[style.colorMode] = this.getColorForValue(v, style);
if (column.style.colorMode) {
this.colorState[column.style.colorMode] = this.getColorForValue(v, column.style);
}
return valueFormater(v, style.decimals, null);
return valueFormatter(v, column.style.decimals, null);
};
}
return (value) => {
return this.defaultCellFormater(value, style);
return this.defaultCellFormatter(value, column.style);
};
}
formatColumnValue(colIndex, value) {
if (this.formaters[colIndex]) {
return this.formaters[colIndex](value);
}
for (let i = 0; i < this.panel.styles.length; i++) {
let style = this.panel.styles[i];
let column = this.table.columns[colIndex];
var regex = kbn.stringToJsRegex(style.pattern);
if (column.text.match(regex)) {
this.formaters[colIndex] = this.createColumnFormater(style, column);
return this.formaters[colIndex](value);
}
}
this.formaters[colIndex] = this.defaultCellFormater;
return this.formaters[colIndex](value);
return this.formatters[colIndex] ? this.formatters[colIndex](value) : value;
}
renderCell(columnIndex, value, addWidthHack = false) {
......@@ -126,7 +143,7 @@ export class TableRenderer {
// this hack adds header content to cell (not visible)
var widthHack = '';
if (addWidthHack) {
widthHack = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].text + '</div>';
widthHack = '<div class="table-panel-width-hack">' + this.table.columns[columnIndex].title + '</div>';
}
if (value === undefined) {
......
......@@ -22,13 +22,15 @@ describe('when rendering table', () => {
{
pattern: 'Time',
type: 'date',
format: 'LLL'
format: 'LLL',
alias: 'Timestamp'
},
{
pattern: 'Value',
pattern: '/(Val)ue/',
type: 'number',
unit: 'ms',
decimals: 3,
alias: '$1'
},
{
pattern: 'Colored',
......@@ -132,6 +134,18 @@ describe('when rendering table', () => {
var html = renderer.renderCell(6, 'text <a href="http://google.com">link</a>');
expect(html).to.be('<td>sanitized</td>');
});
it('Time column title should be Timestamp', () => {
expect(table.columns[0].title).to.be('Timestamp');
});
it('Value column title should be Val', () => {
expect(table.columns[1].title).to.be('Val');
});
it('Colored column title should be Colored', () => {
expect(table.columns[2].title).to.be('Colored');
});
});
});
......
......@@ -220,8 +220,7 @@ transformers['json'] = {
};
function transformDataToTable(data, panel) {
var model = new TableModel(),
copyData = angular.copy(data);
var model = new TableModel();
if (!data || data.length === 0) {
return model;
......@@ -229,16 +228,10 @@ function transformDataToTable(data, panel) {
var transformer = transformers[panel.transform];
if (!transformer) {
throw {message: 'Transformer ' + panel.transformer + ' not found'};
throw {message: 'Transformer ' + panel.transform + ' not found'};
}
if (panel.filterNull) {
for (var i = 0; i < copyData.length; i++) {
copyData[i].datapoints = copyData[i].datapoints.filter((dp) => dp[0] != null);
}
}
transformer.transform(copyData, panel, model);
transformer.transform(data, panel, model);
return model;
}
......
......@@ -68,3 +68,12 @@
top: 1px;
}
}
.form-tabs-wrapper {
@include brand-bottom-border();
@include clearfix();
}
.form-tabs-content {
padding: $spacer*2 $spacer;
}
......@@ -10,7 +10,8 @@
}
.edit-sidemenu-aside {
min-width: 15rem;
min-width: 6rem;
margin-right: $spacer*2;
}
.edit-sidemenu {
......
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