diff --git a/CHANGELOG.md b/CHANGELOG.md index 30bcc05597a..022684f9fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index c2896c5b172..8d77e5c59c0 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -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, diff --git a/docs/sources/features/panels/table_panel.md b/docs/sources/features/panels/table_panel.md index 9ddc2cddaee..69cd02fdbcc 100644 --- a/docs/sources/features/panels/table_panel.md +++ b/docs/sources/features/panels/table_panel.md @@ -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. diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 053d4a8d329..a7b7ae87a62 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -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 +```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 - } +{ + "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 +```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 +{ + "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 diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 2071ff1b79c..7bbf8c2052f 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -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 diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index c3e51dc7a0c..2e9442c15a0 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -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; }); diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 3e446b14b9e..4c098f60a4c 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -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) { diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index f2f0192e034..24b501eee28 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -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 diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.js b/public/app/plugins/datasource/elasticsearch/bucket_agg.js index d6d641c1655..b2cfc819579 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.js +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.js @@ -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') { diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js index fbbd4f7e929..dd296c00a34 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.js +++ b/public/app/plugins/datasource/elasticsearch/query_builder.js @@ -119,7 +119,7 @@ function (queryDef) { } query.script_fields = {}, - query.fielddata_fields = [this.timeField]; + query.docvalue_fields = [this.timeField]; return query; }; diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index bff03fbb7d5..31bc7e6d8ad 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -11,7 +11,7 @@ - +
diff --git a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts index 3467c5b7542..3bb3c04d122 100644 --- a/public/app/plugins/panel/heatmap/specs/renderer_specs.ts +++ b/public/app/plugins/panel/heatmap/specs/renderer_specs.ts @@ -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); } diff --git a/public/app/plugins/panel/table/column_options.html b/public/app/plugins/panel/table/column_options.html new file mode 100644 index 00000000000..35fa4ac2d8b --- /dev/null +++ b/public/app/plugins/panel/table/column_options.html @@ -0,0 +1,99 @@ + +
+ + +
+ +
+
Options
+
+
+ + +
+
+
+ + +
+
+ +
+
Type
+ +
+ +
+ +
+
+
+ + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
Thresholds
+
+ + +
+
+ +
+ +
+
+
+ + + + + + + + + + +
+ Invert +
+
+
+ +
+ + +
+
+ +
diff --git a/public/app/plugins/panel/table/column_options.ts b/public/app/plugins/panel/table/column_options.ts new file mode 100644 index 00000000000..5109f87f86d --- /dev/null +++ b/public/app/plugins/panel/table/column_options.ts @@ -0,0 +1,120 @@ +/// + + +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, + }; +} diff --git a/public/app/plugins/panel/table/editor.html b/public/app/plugins/panel/table/editor.html index 448669ca6a9..9854ac26dc3 100644 --- a/public/app/plugins/panel/table/editor.html +++ b/public/app/plugins/panel/table/editor.html @@ -21,120 +21,27 @@
-
-
Table Display
-
-
- - -
- -
- -
- -
-
-
-
- - -
-
-
Column Styles
-
-
-
- - -
-
- -
- -
-
-
- - -
- -
-
-
-
- -
-
- -
-
- -
-
-
-
-
- - -
-
- -
- -
-
-
-
-
+
Paging
+
+ + +
+ +
+ +
+
- -
-
- - -
-
- - - - - - - - - - -
-
-
- Invert -
-
-
-
-
- -
diff --git a/public/app/plugins/panel/table/editor.ts b/public/app/plugins/panel/table/editor.ts index 6b803cc0444..ce5d0c1c828 100644 --- a/public/app/plugins/panel/table/editor.ts +++ b/public/app/plugins/panel/table/editor.ts @@ -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 */ diff --git a/public/app/plugins/panel/table/module.html b/public/app/plugins/panel/table/module.html index e405c6c42e3..5c6fcbfdb1e 100644 --- a/public/app/plugins/panel/table/module.html +++ b/public/app/plugins/panel/table/module.html @@ -7,7 +7,7 @@
- {{col.text}} + {{col.title}} diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index bf872b47d27..8acf14f56ca 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -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) { diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 2f5a51ab111..ae56e8dff12 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -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 = '
' + this.table.columns[columnIndex].text + '
'; + widthHack = '
' + this.table.columns[columnIndex].title + '
'; } if (value === undefined) { diff --git a/public/app/plugins/panel/table/specs/renderer_specs.ts b/public/app/plugins/panel/table/specs/renderer_specs.ts index 46789f295d1..6b031d4bb91 100644 --- a/public/app/plugins/panel/table/specs/renderer_specs.ts +++ b/public/app/plugins/panel/table/specs/renderer_specs.ts @@ -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 link'); expect(html).to.be('sanitized'); }); + + 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'); + }); }); }); diff --git a/public/app/plugins/panel/table/transformers.ts b/public/app/plugins/panel/table/transformers.ts index 15cc9e71134..c7f957e0156 100644 --- a/public/app/plugins/panel/table/transformers.ts +++ b/public/app/plugins/panel/table/transformers.ts @@ -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; } diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index 2426f70afee..32e395c4d73 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -68,3 +68,12 @@ top: 1px; } } + +.form-tabs-wrapper { + @include brand-bottom-border(); + @include clearfix(); +} + +.form-tabs-content { + padding: $spacer*2 $spacer; +} diff --git a/public/sass/components/edit_sidemenu.scss b/public/sass/components/edit_sidemenu.scss index 83f845c08e1..d7844ab6f36 100644 --- a/public/sass/components/edit_sidemenu.scss +++ b/public/sass/components/edit_sidemenu.scss @@ -10,7 +10,8 @@ } .edit-sidemenu-aside { - min-width: 15rem; + min-width: 6rem; + margin-right: $spacer*2; } .edit-sidemenu {