Commit acff78d4 by Torkel Ödegaard

Merge branch 'master' into heatmap-refactoring2

parents fbf39598 8bbff2c4
...@@ -10,7 +10,6 @@ ...@@ -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) * **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) * **Single Stat Panel**: support for non time series data [#6564](https://github.com/grafana/grafana/issues/6564)
## Minor Enchancements ## Minor Enchancements
* **Prometheus**: Make Prometheus query field a textarea [#7663](https://github.com/grafana/grafana/issues/7663), thx [@hagen1778](https://github.com/hagen1778) * **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 @@ ...@@ -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) * **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) * **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) * **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 ## Fixes
* **Table Panel**: Fixed annotation display in table panel, [#8023](https://github.com/grafana/grafana/issues/8023) * **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) * **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) * **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) # 4.2.0 (2017-03-22)
## Minor Enhancements ## Minor Enhancements
* **Templates**: Prevent use of the prefix `__` for templates in web UI [#7678](https://github.com/grafana/grafana/issues/7678) * **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 ...@@ -13,29 +13,26 @@ weight = 10
# Using AWS CloudWatch in Grafana # 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 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.
be ready to build dashboards for you CloudWatch metrics.
## Adding the data source ## Adding the data source to Grafana
![](/img/docs/cloudwatch/cloudwatch_add.png)
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`. 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. > 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).
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/).
Name | Description 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. *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. *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 *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) *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 *Custom Metrics namespace* | Specify the CloudWatch namespace of Custom metrics
Assume Role Arn | Specify the ARN of the role to assume *Assume Role Arn* | Specify the ARN of the role to assume
## Authentication ## Authentication
...@@ -61,49 +58,64 @@ Example content: ...@@ -61,49 +58,64 @@ Example content:
## Metric Query Editor ## 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. You need to specify a namespace, metric, at least one stat, and at least one dimension.
## Templated queries ## 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 Name | Description
------- | -------- ------- | --------
`regions()` | Returns a list of regions AWS provides their service. *regions()* | Returns a list of regions AWS provides their service.
`namespaces()` | Returns a list of namespaces CloudWatch support. *namespaces()* | Returns a list of namespaces CloudWatch support.
`metrics(namespace, [region])` | Returns a list of metrics in the namespace. (specify region for custom metrics) *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_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`. *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`. *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`. *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). 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: 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)` *dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)* | ELB
ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)` *dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)* | ElastiCache
RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)` *dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)* | RedShift
RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)` *dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)* | RDS
S3 | `dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)` *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. 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). 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 Example `ec2_instance_attribute()` query
ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] }) ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] })
![](/img/docs/v2/cloudwatch_templating.png)
## Cost ## Cost
Amazon provides 1 million CloudWatch API requests each month at no additional charge. Past this, 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. ...@@ -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. 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`. 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`. 3. `Title`: Title for the column, when using a Regex the title can include replacement strings like `$1`.
4. `Coloring` and `Thresholds`: Specify color mode and thresholds limits. 4. `Format`: Specify date format. Only available when `Type` is set to `Date`.
5. `Unit` and `Decimals`: Specify unit and decimal precision for numbers. 5. `Coloring` and `Thresholds`: Specify color mode and thresholds limits.
6. `Add column style rule`: Add new column rule. 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. ...@@ -19,26 +19,28 @@ Creates a new dashboard or updates an existing dashboard.
**Example Request for new dashboard**: **Example Request for new dashboard**:
POST /api/dashboards/db HTTP/1.1 ```http
Accept: application/json POST /api/dashboards/db HTTP/1.1
Content-Type: application/json Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"dashboard": { {
"id": null, "dashboard": {
"title": "Production Overview", "id": null,
"tags": [ "templated" ], "title": "Production Overview",
"timezone": "browser", "tags": [ "templated" ],
"rows": [ "timezone": "browser",
{ "rows": [
} {
], }
"schemaVersion": 6, ],
"version": 0 "schemaVersion": 6,
}, "version": 0
"overwrite": false },
} "overwrite": false
}
```
JSON Body schema: JSON Body schema:
...@@ -47,15 +49,17 @@ JSON Body schema: ...@@ -47,15 +49,17 @@ JSON Body schema:
**Example Response**: **Example Response**:
HTTP/1.1 200 OK ```http
Content-Type: application/json; charset=UTF-8 HTTP/1.1 200 OK
Content-Length: 78 Content-Type: application/json; charset=UTF-8
Content-Length: 78
{ {
"slug": "production-overview", "slug": "production-overview",
"status": "success", "status": "success",
"version": 1 "version": 1
} }
```
Status Codes: Status Codes:
...@@ -67,14 +71,16 @@ 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 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: 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 ```http
Content-Type: application/json; charset=UTF-8 HTTP/1.1 412 Precondition Failed
Content-Length: 97 Content-Type: application/json; charset=UTF-8
Content-Length: 97
{ {
"message": "The dashboard has been changed by someone else", "message": "The dashboard has been changed by someone else",
"status": "version-mismatch" "status": "version-mismatch"
} }
```
In in case of title already exists the `status` property will be `name-exists`. 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 ...@@ -86,34 +92,38 @@ Will return the dashboard given the dashboard slug. Slug is the url friendly ver
**Example Request**: **Example Request**:
GET /api/dashboards/db/production-overview HTTP/1.1 ```http
Accept: application/json GET /api/dashboards/db/production-overview HTTP/1.1
Content-Type: application/json Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**: **Example Response**:
HTTP/1.1 200 ```http
Content-Type: application/json HTTP/1.1 200
Content-Type: application/json
{
"meta": { {
"isStarred": false, "meta": {
"slug": "production-overview" "isStarred": false,
}, "slug": "production-overview"
"dashboard": { },
"id": null, "dashboard": {
"title": "Production Overview", "id": null,
"tags": [ "templated" ], "title": "Production Overview",
"timezone": "browser", "tags": [ "templated" ],
"rows": [ "timezone": "browser",
{ "rows": [
} {
],
"schemaVersion": 6,
"version": 0
} }
} ],
"schemaVersion": 6,
"version": 0
}
}
```
## Delete dashboard ## Delete dashboard
...@@ -123,17 +133,21 @@ The above will delete the dashboard with the specified slug. The slug is the url ...@@ -123,17 +133,21 @@ The above will delete the dashboard with the specified slug. The slug is the url
**Example Request**: **Example Request**:
DELETE /api/dashboards/db/test HTTP/1.1 ```http
Accept: application/json DELETE /api/dashboards/db/test HTTP/1.1
Content-Type: application/json Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**: **Example Response**:
HTTP/1.1 200 ```http
Content-Type: application/json HTTP/1.1 200
Content-Type: application/json
{"title": "Test"} {"title": "Test"}
```
## Gets the home dashboard ## Gets the home dashboard
......
...@@ -22,10 +22,10 @@ installation. ...@@ -22,10 +22,10 @@ installation.
## Install Stable ## Install Stable
``` ```bash
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.2.0_amd64.deb 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 apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_4.2.0_amd64.deb sudo dpkg -i grafana_4.2.0_amd64.deb
``` ```
## APT Repository ## APT Repository
...@@ -43,18 +43,24 @@ candidates. ...@@ -43,18 +43,24 @@ candidates.
Then add the [Package Cloud](https://packagecloud.io/grafana) key. This Then add the [Package Cloud](https://packagecloud.io/grafana) key. This
allows you to install signed packages. 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 Update your Apt repositories and install Grafana
$ sudo apt-get update ```bash
$ sudo apt-get install grafana sudo apt-get update
sudo apt-get install grafana
```
On some older versions of Ubuntu and Debian you may need to install the 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 `apt-transport-https` package which is needed to fetch packages over
HTTPS. HTTPS.
$ sudo apt-get install -y apt-transport-https ```bash
sudo apt-get install -y apt-transport-https
```
## Package details ## Package details
...@@ -70,7 +76,9 @@ HTTPS. ...@@ -70,7 +76,9 @@ HTTPS.
Start Grafana by running: 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, This will start the `grafana-server` process as the `grafana` user,
which was created during the package installation. The default HTTP port which was created during the package installation. The default HTTP port
...@@ -78,19 +86,25 @@ is `3000` and default user and group is `admin`. ...@@ -78,19 +86,25 @@ is `3000` and default user and group is `admin`.
To configure the Grafana server to start at boot time: 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) ## Start the server (via systemd)
To start the service using systemd: To start the service using systemd:
$ systemctl daemon-reload ```bash
$ systemctl start grafana-server systemctl daemon-reload
$ systemctl status grafana-server systemctl start grafana-server
systemctl status grafana-server
```
Enable the systemd service so that Grafana starts at boot. 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 ## Environment file
......
...@@ -77,7 +77,7 @@ function (_, $, coreModule) { ...@@ -77,7 +77,7 @@ function (_, $, coreModule) {
$scope.source = function(query, callback) { $scope.source = function(query, callback) {
$scope.$apply(function() { $scope.$apply(function() {
$scope.getOptions({ measurementFilter: query }).then(function(altSegments) { $scope.getOptions({ $query: query }).then(function(altSegments) {
$scope.altSegments = altSegments; $scope.altSegments = altSegments;
options = _.map($scope.altSegments, function(alt) { return alt.value; }); options = _.map($scope.altSegments, function(alt) { return alt.value; });
......
...@@ -61,7 +61,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ ...@@ -61,7 +61,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
attrs: {dashboard: "ctrl.dashboard", panel: "panel", row: "ctrl.row"}, attrs: {dashboard: "ctrl.dashboard", panel: "panel", row: "ctrl.row"},
}; };
var panelElemName = 'panel-' + scope.panel.type;
let panelInfo = config.panels[scope.panel.type]; let panelInfo = config.panels[scope.panel.type];
var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl); var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
if (panelInfo) { if (panelInfo) {
......
...@@ -53,7 +53,7 @@ export function exportTableDataToCsv(table) { ...@@ -53,7 +53,7 @@ export function exportTableDataToCsv(table) {
var text = 'sep=;\n'; var text = 'sep=;\n';
// add header // add header
_.each(table.columns, function(column) { _.each(table.columns, function(column) {
text += column.text + ';'; text += (column.title || column.text) + ';';
}); });
text += '\n'; text += '\n';
// process data // process data
......
...@@ -81,9 +81,9 @@ function (angular, _, queryDef) { ...@@ -81,9 +81,9 @@ function (angular, _, queryDef) {
switch($scope.agg.type) { switch($scope.agg.type) {
case 'terms': { case 'terms': {
settings.order = settings.order || "asc"; settings.order = settings.order || "desc";
settings.size = settings.size || "10"; 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"; settings.orderBy = settings.orderBy || "_term";
if (settings.size !== '0') { if (settings.size !== '0') {
......
...@@ -119,7 +119,7 @@ function (queryDef) { ...@@ -119,7 +119,7 @@ function (queryDef) {
} }
query.script_fields = {}, query.script_fields = {},
query.fielddata_fields = [this.timeField]; query.docvalue_fields = [this.timeField];
return query; return query;
}; };
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<label class="gf-form-label query-keyword width-7">FROM</label> <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.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>
<div class="gf-form"> <div class="gf-form">
......
...@@ -81,8 +81,8 @@ describe('grafanaHeatmap', function () { ...@@ -81,8 +81,8 @@ describe('grafanaHeatmap', function () {
getTimezone: sinon.stub().returns('utc') getTimezone: sinon.stub().returns('utc')
}, },
range: { range: {
from: moment.utc("01 Mar 2017 10: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"), to: moment.utc("01 Mar 2017 11:00:00", 'DD MMM YYYY HH:mm:ss'),
}, },
}; };
...@@ -263,5 +263,5 @@ function getTicks(element, axisSelector) { ...@@ -263,5 +263,5 @@ function getTicks(element, axisSelector) {
function formatLocalTime(timeStr) { function formatLocalTime(timeStr) {
let format = "HH:mm"; 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 @@ ...@@ -21,120 +21,27 @@
<metric-segment segment="editor.addColumnSegment" get-options="editor.getColumnOptions()" on-change="editor.addColumn()"></metric-segment> <metric-segment segment="editor.addColumnSegment" get-options="editor.getColumnOptions()" on-change="editor.addColumn()"></metric-segment>
</div> </div>
</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>
<div class="section gf-form-group"> <div class="section gf-form-group">
<h5 class="section-heading">Table Display</h5> <h5 class="section-heading">Paging</h5>
<div class="gf-form-inline"> <div class="gf-form">
<div class="gf-form"> <label class="gf-form-label width-8">Rows per page</label>
<label class="gf-form-label width-8">Rows per page</label> <input type="number" class="gf-form-input width-6"
<input type="number" class="gf-form-input width-6" placeholder="100" data-placement="right"
placeholder="100" data-placement="right" ng-model="editor.panel.pageSize"
ng-model="editor.panel.pageSize" ng-change="editor.render()"
ng-change="editor.render()" ng-model-onblur>
ng-model-onblur> </div>
</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>
<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">
<div class="gf-form max-width-17"> <label class="gf-form-label width-8">Font size</label>
<label class="gf-form-label width-6">Font size</label> <div class="gf-form-select-wrapper width-6">
<div class="gf-form-select-wrapper max-width-15"> <select class="gf-form-input"
<select class="gf-form-input" ng-model="editor.panel.fontSize"
ng-model="editor.panel.fontSize" ng-options="f for f in editor.fontSizes"
ng-options="f for f in editor.fontSizes" ng-change="editor.render()"></select>
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>
</div> </div>
</div> </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> </div>
...@@ -13,13 +13,8 @@ export class TablePanelEditorCtrl { ...@@ -13,13 +13,8 @@ export class TablePanelEditorCtrl {
panel: any; panel: any;
panelCtrl: any; panelCtrl: any;
transformers: any; transformers: any;
colorModes: any;
columnStyles: any;
columnTypes: any;
fontSizes: any; fontSizes: any;
dateFormats: any;
addColumnSegment: any; addColumnSegment: any;
unitFormats: any;
getColumnNames: any; getColumnNames: any;
/** @ngInject */ /** @ngInject */
...@@ -28,37 +23,9 @@ export class TablePanelEditorCtrl { ...@@ -28,37 +23,9 @@ export class TablePanelEditorCtrl {
this.panelCtrl = $scope.ctrl; this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel; this.panel = this.panelCtrl.panel;
this.transformers = transformers; 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.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.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() { getColumnOptions() {
...@@ -97,38 +64,6 @@ export class TablePanelEditorCtrl { ...@@ -97,38 +64,6 @@ export class TablePanelEditorCtrl {
this.panel.columns = _.without(this.panel.columns, column); this.panel.columns = _.without(this.panel.columns, column);
this.panelCtrl.render(); 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 */ /** @ngInject */
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<tr> <tr>
<th ng-repeat="col in ctrl.table.columns" ng-hide="col.hidden"> <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)"> <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"> <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-down" ng-show="col.desc"></i>
<i class="fa fa-caret-up" ng-hide="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'; ...@@ -8,6 +8,7 @@ import * as FileExport from 'app/core/utils/file_export';
import {MetricsPanelCtrl} from 'app/plugins/sdk'; import {MetricsPanelCtrl} from 'app/plugins/sdk';
import {transformDataToTable} from './transformers'; import {transformDataToTable} from './transformers';
import {tablePanelEditor} from './editor'; import {tablePanelEditor} from './editor';
import {columnOptionsTab} from './column_options';
import {TableRenderer} from './renderer'; import {TableRenderer} from './renderer';
class TablePanelCtrl extends MetricsPanelCtrl { class TablePanelCtrl extends MetricsPanelCtrl {
...@@ -16,6 +17,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -16,6 +17,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
pageIndex: number; pageIndex: number;
dataRaw: any; dataRaw: any;
table: any; table: any;
renderer: any;
panelDefaults = { panelDefaults = {
targets: [{}], targets: [{}],
...@@ -26,11 +28,13 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -26,11 +28,13 @@ class TablePanelCtrl extends MetricsPanelCtrl {
{ {
type: 'date', type: 'date',
pattern: 'Time', pattern: 'Time',
alias: 'Time',
dateFormat: 'YYYY-MM-DD HH:mm:ss', dateFormat: 'YYYY-MM-DD HH:mm:ss',
}, },
{ {
unit: 'short', unit: 'short',
type: 'number', type: 'number',
alias: '',
decimals: 2, decimals: 2,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
colorMode: null, colorMode: null,
...@@ -42,7 +46,6 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -42,7 +46,6 @@ class TablePanelCtrl extends MetricsPanelCtrl {
scroll: true, scroll: true,
fontSize: '100%', fontSize: '100%',
sort: {col: 0, desc: true}, sort: {col: 0, desc: true},
filterNull: false,
}; };
/** @ngInject */ /** @ngInject */
...@@ -68,6 +71,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -68,6 +71,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
onInitEditMode() { onInitEditMode() {
this.addEditorTab('Options', tablePanelEditor, 2); this.addEditorTab('Options', tablePanelEditor, 2);
this.addEditorTab('Column Styles', columnOptionsTab, 3);
} }
onInitPanelActions(actions) { onInitPanelActions(actions) {
...@@ -118,6 +122,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -118,6 +122,9 @@ class TablePanelCtrl extends MetricsPanelCtrl {
render() { render() {
this.table = transformDataToTable(this.dataRaw, this.panel); this.table = transformDataToTable(this.dataRaw, this.panel);
this.table.sort(this.panel.sort); this.table.sort(this.panel.sort);
this.renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize);
return super.render(this.table); return super.render(this.table);
} }
...@@ -141,8 +148,7 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -141,8 +148,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
} }
exportCsv() { exportCsv() {
var renderer = new TableRenderer(this.panel, this.table, this.dashboard.isTimezoneUtc(), this.$sanitize); FileExport.exportTableDataToCsv(this.renderer.render_values());
FileExport.exportTableDataToCsv(renderer.render_values());
} }
link(scope, elem, attrs, ctrl) { link(scope, elem, attrs, ctrl) {
...@@ -162,9 +168,9 @@ class TablePanelCtrl extends MetricsPanelCtrl { ...@@ -162,9 +168,9 @@ class TablePanelCtrl extends MetricsPanelCtrl {
} }
function appendTableRows(tbodyElem) { function appendTableRows(tbodyElem) {
var renderer = new TableRenderer(panel, data, ctrl.dashboard.isTimezoneUtc(), ctrl.$sanitize); ctrl.renderer.setTable(data);
tbodyElem.empty(); tbodyElem.empty();
tbodyElem.html(renderer.render(ctrl.pageIndex)); tbodyElem.html(ctrl.renderer.render(ctrl.pageIndex));
} }
function switchPage(e) { function switchPage(e) {
......
...@@ -5,12 +5,44 @@ import moment from 'moment'; ...@@ -5,12 +5,44 @@ import moment from 'moment';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
export class TableRenderer { export class TableRenderer {
formaters: any[]; formatters: any[];
colorState: any; colorState: any;
constructor(private panel, private table, private isUtc, private sanitize) { 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 = {}; 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) { getColorForValue(value, style) {
...@@ -24,7 +56,7 @@ export class TableRenderer { ...@@ -24,7 +56,7 @@ export class TableRenderer {
return _.first(style.colors); return _.first(style.colors);
} }
defaultCellFormater(v, style) { defaultCellFormatter(v, style) {
if (v === null || v === void 0 || v === undefined) { if (v === null || v === void 0 || v === undefined) {
return ''; return '';
} }
...@@ -40,18 +72,18 @@ export class TableRenderer { ...@@ -40,18 +72,18 @@ export class TableRenderer {
} }
} }
createColumnFormater(style, column) { createColumnFormatter(column) {
if (!style) { if (!column.style) {
return this.defaultCellFormater; return this.defaultCellFormatter;
} }
if (style.type === 'hidden') { if (column.style.type === 'hidden') {
return v => { return v => {
return undefined; return undefined;
}; };
} }
if (style.type === 'date') { if (column.style.type === 'date') {
return v => { return v => {
if (v === undefined || v === null) { if (v === undefined || v === null) {
return '-'; return '-';
...@@ -62,12 +94,12 @@ export class TableRenderer { ...@@ -62,12 +94,12 @@ export class TableRenderer {
if (this.isUtc) { if (this.isUtc) {
date = date.utc(); date = date.utc();
} }
return date.format(style.dateFormat); return date.format(column.style.dateFormat);
}; };
} }
if (style.type === 'number') { if (column.style.type === 'number') {
let valueFormater = kbn.valueFormats[column.unit || style.unit]; let valueFormatter = kbn.valueFormats[column.unit || column.style.unit];
return v => { return v => {
if (v === null || v === void 0) { if (v === null || v === void 0) {
...@@ -75,39 +107,24 @@ export class TableRenderer { ...@@ -75,39 +107,24 @@ export class TableRenderer {
} }
if (_.isString(v)) { if (_.isString(v)) {
return this.defaultCellFormater(v, style); return this.defaultCellFormatter(v, column.style);
} }
if (style.colorMode) { if (column.style.colorMode) {
this.colorState[style.colorMode] = this.getColorForValue(v, style); 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 (value) => {
return this.defaultCellFormater(value, style); return this.defaultCellFormatter(value, column.style);
}; };
} }
formatColumnValue(colIndex, value) { formatColumnValue(colIndex, value) {
if (this.formaters[colIndex]) { return this.formatters[colIndex] ? this.formatters[colIndex](value) : value;
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);
} }
renderCell(columnIndex, value, addWidthHack = false) { renderCell(columnIndex, value, addWidthHack = false) {
...@@ -126,7 +143,7 @@ export class TableRenderer { ...@@ -126,7 +143,7 @@ export class TableRenderer {
// this hack adds header content to cell (not visible) // this hack adds header content to cell (not visible)
var widthHack = ''; var widthHack = '';
if (addWidthHack) { 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) { if (value === undefined) {
......
...@@ -22,13 +22,15 @@ describe('when rendering table', () => { ...@@ -22,13 +22,15 @@ describe('when rendering table', () => {
{ {
pattern: 'Time', pattern: 'Time',
type: 'date', type: 'date',
format: 'LLL' format: 'LLL',
alias: 'Timestamp'
}, },
{ {
pattern: 'Value', pattern: '/(Val)ue/',
type: 'number', type: 'number',
unit: 'ms', unit: 'ms',
decimals: 3, decimals: 3,
alias: '$1'
}, },
{ {
pattern: 'Colored', pattern: 'Colored',
...@@ -132,6 +134,18 @@ describe('when rendering table', () => { ...@@ -132,6 +134,18 @@ describe('when rendering table', () => {
var html = renderer.renderCell(6, 'text <a href="http://google.com">link</a>'); var html = renderer.renderCell(6, 'text <a href="http://google.com">link</a>');
expect(html).to.be('<td>sanitized</td>'); 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'] = { ...@@ -220,8 +220,7 @@ transformers['json'] = {
}; };
function transformDataToTable(data, panel) { function transformDataToTable(data, panel) {
var model = new TableModel(), var model = new TableModel();
copyData = angular.copy(data);
if (!data || data.length === 0) { if (!data || data.length === 0) {
return model; return model;
...@@ -229,16 +228,10 @@ function transformDataToTable(data, panel) { ...@@ -229,16 +228,10 @@ function transformDataToTable(data, panel) {
var transformer = transformers[panel.transform]; var transformer = transformers[panel.transform];
if (!transformer) { if (!transformer) {
throw {message: 'Transformer ' + panel.transformer + ' not found'}; throw {message: 'Transformer ' + panel.transform + ' not found'};
} }
if (panel.filterNull) { transformer.transform(data, panel, model);
for (var i = 0; i < copyData.length; i++) {
copyData[i].datapoints = copyData[i].datapoints.filter((dp) => dp[0] != null);
}
}
transformer.transform(copyData, panel, model);
return model; return model;
} }
......
...@@ -68,3 +68,12 @@ ...@@ -68,3 +68,12 @@
top: 1px; top: 1px;
} }
} }
.form-tabs-wrapper {
@include brand-bottom-border();
@include clearfix();
}
.form-tabs-content {
padding: $spacer*2 $spacer;
}
...@@ -10,7 +10,8 @@ ...@@ -10,7 +10,8 @@
} }
.edit-sidemenu-aside { .edit-sidemenu-aside {
min-width: 15rem; min-width: 6rem;
margin-right: $spacer*2;
} }
.edit-sidemenu { .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