Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
742e80d713 | ||
|
|
eb6f149f8b | ||
|
|
d4d297862e | ||
|
|
06cb92bbb8 | ||
|
|
5a758213c6 | ||
|
|
20691c9aae | ||
|
|
624be3450d | ||
|
|
63e36b1656 | ||
|
|
433a059bcf | ||
|
|
1862c5ac17 | ||
|
|
6665b7e18a | ||
|
|
4994f2f956 | ||
|
|
8d2039bf37 | ||
|
|
12492216d6 | ||
|
|
51623fdc72 | ||
|
|
27b674640b | ||
|
|
803504befd | ||
|
|
570fe11341 | ||
|
|
0537671450 | ||
|
|
089f42d940 | ||
|
|
2158753c5e | ||
|
|
dee408e7a3 | ||
|
|
c37d116550 | ||
|
|
a1a06ea78a | ||
|
|
f813f4cea7 | ||
|
|
3ffe5554f3 | ||
|
|
e1abad408e | ||
|
|
188d912653 | ||
|
|
8d917d0c1e | ||
|
|
f6973a81b8 | ||
|
|
f13adb369b | ||
|
|
cb5d353b36 | ||
|
|
e5249b8852 | ||
|
|
8dde6da21a | ||
|
|
3c3931b2c0 | ||
|
|
77ab254a5e | ||
|
|
12b974a2bc | ||
|
|
cc4b908afd | ||
|
|
cda8fdfe8d | ||
|
|
ec8f8e386a | ||
|
|
76dc89b2dc | ||
|
|
ca2fcb582a | ||
|
|
54539cb35f | ||
|
|
8e12d35da9 | ||
|
|
34f65b1455 | ||
|
|
e783d32efa | ||
|
|
a51f237291 | ||
|
|
46ba4be810 | ||
|
|
7ce802f3ef |
@@ -0,0 +1,41 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.1.1
|
||||||
|
|
||||||
|
- Fix for annotations in grafana 5.x before 5.4
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- Tested with Grafana 6.0.0
|
||||||
|
- Tested with InfluxDB and Mysql datasources
|
||||||
|
- Add initial support for display annotations
|
||||||
|
- Add example for k8s statuses (thanks, @vrutkovs)
|
||||||
|
- Fix hanging on big values
|
||||||
|
- Fix horizontal spacing = 0
|
||||||
|
- Fix for "Object doesn't support property or method 'remove'"
|
||||||
|
- Fix card width for targets with different datapoints count
|
||||||
|
|
||||||
|
## v0.0.4
|
||||||
|
|
||||||
|
- Fix display of multivalues buckets as an empty cell
|
||||||
|
|
||||||
|
## v0.0.3
|
||||||
|
|
||||||
|
- Add solarized preset
|
||||||
|
- Reorganize statuses editor for discrete mode
|
||||||
|
- Separate options for vertical and horizontal spacing for cards
|
||||||
|
- Add simple sort options for Y axis
|
||||||
|
- Fix display null values as zero
|
||||||
|
|
||||||
|
## v0.0.2
|
||||||
|
|
||||||
|
- Install with GF_INSTALL_PLUGINS
|
||||||
|
- Fix legend overlap
|
||||||
|
- Fix colors for dark theme
|
||||||
|
- Fix panel rendering timeout error
|
||||||
|
|
||||||
|
|
||||||
|
## v0.0.1
|
||||||
|
|
||||||
|
- First public release
|
||||||
|
|
||||||
@@ -3,8 +3,18 @@ module.exports = (grunt) => {
|
|||||||
|
|
||||||
grunt.loadNpmTasks('grunt-execute');
|
grunt.loadNpmTasks('grunt-execute');
|
||||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
grunt.loadNpmTasks('grunt-contrib-clean');
|
||||||
|
grunt.loadNpmTasks('grunt-notify');
|
||||||
|
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
|
notify: {
|
||||||
|
watch: {
|
||||||
|
options: {
|
||||||
|
message: 'grunt watch Complete',
|
||||||
|
title: 'flant-statusmap-panel rebuilded',
|
||||||
|
duration: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
clean: ['dist'],
|
clean: ['dist'],
|
||||||
|
|
||||||
@@ -17,7 +27,7 @@ module.exports = (grunt) => {
|
|||||||
},
|
},
|
||||||
pluginDef: {
|
pluginDef: {
|
||||||
expand: true,
|
expand: true,
|
||||||
src: ['plugin.json', 'README.md'],
|
src: ['plugin.json', 'README.md', 'CHANGELOG.md'],
|
||||||
dest: 'dist'
|
dest: 'dist'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -25,7 +35,7 @@ module.exports = (grunt) => {
|
|||||||
watch: {
|
watch: {
|
||||||
rebuild_all: {
|
rebuild_all: {
|
||||||
files: ['src/**/*', 'plugin.json'],
|
files: ['src/**/*', 'plugin.json'],
|
||||||
tasks: ['default'],
|
tasks: ['default', 'notify:watch'],
|
||||||
options: {
|
options: {
|
||||||
spawn: false,
|
spawn: false,
|
||||||
livereload: true
|
livereload: true
|
||||||
|
|||||||
@@ -13,10 +13,18 @@
|
|||||||
* Increasing rows/buckets' interval for better visual representation
|
* Increasing rows/buckets' interval for better visual representation
|
||||||
* Representing null values as empty bucket or zero value
|
* Representing null values as empty bucket or zero value
|
||||||
|
|
||||||
|
:calendar: New features are planned in [#62](https://github.com/flant/grafana-statusmap/issues/62)
|
||||||
|
|
||||||
### Supported environment
|
### Supported environment
|
||||||
|
|
||||||
* Prometheus datasource
|
* Tested with datasources:
|
||||||
* Tested with Grafana 5.1.3
|
- Prometheus
|
||||||
|
- InfluxDB
|
||||||
|
- Mysql
|
||||||
|
* Tested with Grafana:
|
||||||
|
- 5.1.3
|
||||||
|
- 5.4.3
|
||||||
|
- 6.0.0
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -49,7 +57,18 @@ above. These objects should have _discrete statuses_ which are sets of predefine
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Prometheus
|
### Datasource notes
|
||||||
|
|
||||||
|
To create neat graphs your datasource should return good data. Plugin adjust `$__interval` variable depending on
|
||||||
|
bucket width in panel options. Your queries should aggregate statuses over `$__interval`.
|
||||||
|
|
||||||
|
To make multiple values mode works as expected you should define multiple queries: one query for each possible status.
|
||||||
|
|
||||||
|
Plugin doesn't aggregate data in time for now, it only renders input data as buckets. Because of this
|
||||||
|
data should contain points for each timestamp in time range and equal timestamps for every possible
|
||||||
|
target (y-axis label). This limitation is addressed by [issue #53](https://github.com/flant/grafana-statusmap/issues/53).
|
||||||
|
|
||||||
|
#### Prometheus
|
||||||
|
|
||||||
To work with data from Prometheus you will need to setup discrete statuses for your objects.
|
To work with data from Prometheus you will need to setup discrete statuses for your objects.
|
||||||
Requirements to store these statuses in metrics are as follows:
|
Requirements to store these statuses in metrics are as follows:
|
||||||
@@ -83,6 +102,28 @@ coffee_maker_status:discrete{status="3"} 1
|
|||||||
Now, when Prometheus has `0` and `1` values for each status, all these metrics can be
|
Now, when Prometheus has `0` and `1` values for each status, all these metrics can be
|
||||||
aggregated, so you will get all available statuses of your objects over time.
|
aggregated, so you will get all available statuses of your objects over time.
|
||||||
|
|
||||||
|
#### InfluxDB
|
||||||
|
|
||||||
|
Choose 'Time series' for 'Format as' and use `GROUP BY ($__interval)` in query. `$tag_<tag name>` can be used in 'Alias by' to define y-axis labels.
|
||||||
|
|
||||||
|
#### Mysql
|
||||||
|
|
||||||
|
Example query with aggregation over `$__interval` is like this (you need one query for each possible status value):
|
||||||
|
|
||||||
|
```
|
||||||
|
SELECT
|
||||||
|
$__timeGroupAlias(date_insert,$__interval),
|
||||||
|
name AS metric,
|
||||||
|
min(statusi) AS "statusi"
|
||||||
|
FROM coffee_makers
|
||||||
|
WHERE
|
||||||
|
$__timeFilter(date_insert) AND statusi=1
|
||||||
|
GROUP BY 1,2
|
||||||
|
ORDER BY $__timeGroup(date_insert,$__interval)
|
||||||
|
```
|
||||||
|
|
||||||
|
`metric` column is used as y-axis label.
|
||||||
|
|
||||||
### Panel
|
### Panel
|
||||||
|
|
||||||
First of all, an individual query for each possible status' value should be created.
|
First of all, an individual query for each possible status' value should be created.
|
||||||
@@ -94,11 +135,20 @@ Then, color mapping for status' values should be defined in __Discrete__ color m
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
Use can use presets to define a trafic light colors or 8 colors from [solarized](https://ethanschoonover.com/solarized/) palette:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
|
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
|
||||||
|
|
||||||
|
|
||||||
### More options
|
### More options
|
||||||
|
|
||||||
|
#### Bucket
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
__Multiple values__ checkbox specifies how they should be displayed:
|
__Multiple values__ checkbox specifies how they should be displayed:
|
||||||
@@ -114,8 +164,19 @@ __Null values__ can be treated as empty buckets or displayed with the color of `
|
|||||||
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
|
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
|
||||||
__Rounding__ may be used to round edges.
|
__Rounding__ may be used to round edges.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Display
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
__Show legend__ checkbox toggles legend at the bottom of the panel.
|
||||||
|
|
||||||
|
__Show tooltip__ toggles tooltip display on mouse over buckets.
|
||||||
|
|
||||||
|
__Y axis sort__ can be used to sort labels on Y axis. Metrics — sort y labels as they are defined on Metrics tab. a→z and z→a sort labels descending or ascending in a [natural](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) order.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -142,15 +203,7 @@ The first public release of this plugin has been fully made by [Flant](https://f
|
|||||||
|
|
||||||
This plugin is based on "Heatmap" panel by Grafana and partly inspired by ideas from Carpet plot, Discrete panel, Status Panel, Status Dot, Status By Group.
|
This plugin is based on "Heatmap" panel by Grafana and partly inspired by ideas from Carpet plot, Discrete panel, Status Panel, Status Dot, Status By Group.
|
||||||
|
|
||||||
#### Changelog
|
|
||||||
|
|
||||||
##### v0.0.2
|
|
||||||
|
|
||||||
- Install with GF_INSTALL_PLUGINS
|
|
||||||
- Fix legend overlap
|
|
||||||
|
|
||||||
##### v0.0.1
|
|
||||||
|
|
||||||
- First public release
|
|
||||||
|
|
||||||
|
### CHANGELOG
|
||||||
|
|
||||||
|
The latest changes can be found here: [CHANGELOG.md](https://github.com/flant/grafana-statusmap/blob/master/CHANGELOG.md)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v0.1.1
|
||||||
|
|
||||||
|
- Fix for annotations in grafana 5.x before 5.4
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- Tested with Grafana 6.0.0
|
||||||
|
- Tested with InfluxDB and Mysql datasources
|
||||||
|
- Add initial support for display annotations
|
||||||
|
- Add example for k8s statuses (thanks, @vrutkovs)
|
||||||
|
- Fix hanging on big values
|
||||||
|
- Fix horizontal spacing = 0
|
||||||
|
- Fix for "Object doesn't support property or method 'remove'"
|
||||||
|
- Fix card width for targets with different datapoints count
|
||||||
|
|
||||||
|
## v0.0.4
|
||||||
|
|
||||||
|
- Fix display of multivalues buckets as an empty cell
|
||||||
|
|
||||||
|
## v0.0.3
|
||||||
|
|
||||||
|
- Add solarized preset
|
||||||
|
- Reorganize statuses editor for discrete mode
|
||||||
|
- Separate options for vertical and horizontal spacing for cards
|
||||||
|
- Add simple sort options for Y axis
|
||||||
|
- Fix display null values as zero
|
||||||
|
|
||||||
|
## v0.0.2
|
||||||
|
|
||||||
|
- Install with GF_INSTALL_PLUGINS
|
||||||
|
- Fix legend overlap
|
||||||
|
- Fix colors for dark theme
|
||||||
|
- Fix panel rendering timeout error
|
||||||
|
|
||||||
|
|
||||||
|
## v0.0.1
|
||||||
|
|
||||||
|
- First public release
|
||||||
|
|
||||||
@@ -13,10 +13,18 @@
|
|||||||
* Increasing rows/buckets' interval for better visual representation
|
* Increasing rows/buckets' interval for better visual representation
|
||||||
* Representing null values as empty bucket or zero value
|
* Representing null values as empty bucket or zero value
|
||||||
|
|
||||||
|
:calendar: New features are planned in [#62](https://github.com/flant/grafana-statusmap/issues/62)
|
||||||
|
|
||||||
### Supported environment
|
### Supported environment
|
||||||
|
|
||||||
* Prometheus datasource
|
* Tested with datasources:
|
||||||
* Tested with Grafana 5.1.3
|
- Prometheus
|
||||||
|
- InfluxDB
|
||||||
|
- Mysql
|
||||||
|
* Tested with Grafana:
|
||||||
|
- 5.1.3
|
||||||
|
- 5.4.3
|
||||||
|
- 6.0.0
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -49,7 +57,18 @@ above. These objects should have _discrete statuses_ which are sets of predefine
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Prometheus
|
### Datasource notes
|
||||||
|
|
||||||
|
To create neat graphs your datasource should return good data. Plugin adjust `$__interval` variable depending on
|
||||||
|
bucket width in panel options. Your queries should aggregate statuses over `$__interval`.
|
||||||
|
|
||||||
|
To make multiple values mode works as expected you should define multiple queries: one query for each possible status.
|
||||||
|
|
||||||
|
Plugin doesn't aggregate data in time for now, it only renders input data as buckets. Because of this
|
||||||
|
data should contain points for each timestamp in time range and equal timestamps for every possible
|
||||||
|
target (y-axis label). This limitation is addressed by [issue #53](https://github.com/flant/grafana-statusmap/issues/53).
|
||||||
|
|
||||||
|
#### Prometheus
|
||||||
|
|
||||||
To work with data from Prometheus you will need to setup discrete statuses for your objects.
|
To work with data from Prometheus you will need to setup discrete statuses for your objects.
|
||||||
Requirements to store these statuses in metrics are as follows:
|
Requirements to store these statuses in metrics are as follows:
|
||||||
@@ -83,6 +102,28 @@ coffee_maker_status:discrete{status="3"} 1
|
|||||||
Now, when Prometheus has `0` and `1` values for each status, all these metrics can be
|
Now, when Prometheus has `0` and `1` values for each status, all these metrics can be
|
||||||
aggregated, so you will get all available statuses of your objects over time.
|
aggregated, so you will get all available statuses of your objects over time.
|
||||||
|
|
||||||
|
#### InfluxDB
|
||||||
|
|
||||||
|
Choose 'Time series' for 'Format as' and use `GROUP BY ($__interval)` in query. `$tag_<tag name>` can be used in 'Alias by' to define y-axis labels.
|
||||||
|
|
||||||
|
#### Mysql
|
||||||
|
|
||||||
|
Example query with aggregation over `$__interval` is like this (you need one query for each possible status value):
|
||||||
|
|
||||||
|
```
|
||||||
|
SELECT
|
||||||
|
$__timeGroupAlias(date_insert,$__interval),
|
||||||
|
name AS metric,
|
||||||
|
min(statusi) AS "statusi"
|
||||||
|
FROM coffee_makers
|
||||||
|
WHERE
|
||||||
|
$__timeFilter(date_insert) AND statusi=1
|
||||||
|
GROUP BY 1,2
|
||||||
|
ORDER BY $__timeGroup(date_insert,$__interval)
|
||||||
|
```
|
||||||
|
|
||||||
|
`metric` column is used as y-axis label.
|
||||||
|
|
||||||
### Panel
|
### Panel
|
||||||
|
|
||||||
First of all, an individual query for each possible status' value should be created.
|
First of all, an individual query for each possible status' value should be created.
|
||||||
@@ -94,11 +135,20 @@ Then, color mapping for status' values should be defined in __Discrete__ color m
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
Use can use presets to define a trafic light colors or 8 colors from [solarized](https://ethanschoonover.com/solarized/) palette:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
|
_Note: __Spectrum__ and __Opacity__ color modes function the same way they do in [Heatmap](https://grafana.com/plugins/heatmap) plugin._
|
||||||
|
|
||||||
|
|
||||||
### More options
|
### More options
|
||||||
|
|
||||||
|
#### Bucket
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
__Multiple values__ checkbox specifies how they should be displayed:
|
__Multiple values__ checkbox specifies how they should be displayed:
|
||||||
@@ -114,8 +164,19 @@ __Null values__ can be treated as empty buckets or displayed with the color of `
|
|||||||
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
|
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
|
||||||
__Rounding__ may be used to round edges.
|
__Rounding__ may be used to round edges.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Display
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
__Show legend__ checkbox toggles legend at the bottom of the panel.
|
||||||
|
|
||||||
|
__Show tooltip__ toggles tooltip display on mouse over buckets.
|
||||||
|
|
||||||
|
__Y axis sort__ can be used to sort labels on Y axis. Metrics — sort y labels as they are defined on Metrics tab. a→z and z→a sort labels descending or ascending in a [natural](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) order.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -142,15 +203,7 @@ The first public release of this plugin has been fully made by [Flant](https://f
|
|||||||
|
|
||||||
This plugin is based on "Heatmap" panel by Grafana and partly inspired by ideas from Carpet plot, Discrete panel, Status Panel, Status Dot, Status By Group.
|
This plugin is based on "Heatmap" panel by Grafana and partly inspired by ideas from Carpet plot, Discrete panel, Status Panel, Status Dot, Status By Group.
|
||||||
|
|
||||||
#### Changelog
|
|
||||||
|
|
||||||
##### v0.0.2
|
|
||||||
|
|
||||||
- Install with GF_INSTALL_PLUGINS
|
|
||||||
- Fix legend overlap
|
|
||||||
|
|
||||||
##### v0.0.1
|
|
||||||
|
|
||||||
- First public release
|
|
||||||
|
|
||||||
|
### CHANGELOG
|
||||||
|
|
||||||
|
The latest changes can be found here: [CHANGELOG.md](https://github.com/flant/grafana-statusmap/blob/master/CHANGELOG.md)
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_export, _context) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var d3, $, _, kbn, _createClass, TOOLTIP_PADDING_X, TOOLTIP_PADDING_Y, AnnotationTooltip;
|
||||||
|
|
||||||
|
function _classCallCheck(instance, Constructor) {
|
||||||
|
if (!(instance instanceof Constructor)) {
|
||||||
|
throw new TypeError("Cannot call a class as a function");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setters: [function (_d) {
|
||||||
|
d3 = _d.default;
|
||||||
|
}, function (_jquery) {
|
||||||
|
$ = _jquery.default;
|
||||||
|
}, function (_lodash) {
|
||||||
|
_ = _lodash.default;
|
||||||
|
}, function (_appCoreUtilsKbn) {
|
||||||
|
kbn = _appCoreUtilsKbn.default;
|
||||||
|
}],
|
||||||
|
execute: function () {
|
||||||
|
_createClass = function () {
|
||||||
|
function defineProperties(target, props) {
|
||||||
|
for (var i = 0; i < props.length; i++) {
|
||||||
|
var descriptor = props[i];
|
||||||
|
descriptor.enumerable = descriptor.enumerable || false;
|
||||||
|
descriptor.configurable = true;
|
||||||
|
if ("value" in descriptor) descriptor.writable = true;
|
||||||
|
Object.defineProperty(target, descriptor.key, descriptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return function (Constructor, protoProps, staticProps) {
|
||||||
|
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
||||||
|
if (staticProps) defineProperties(Constructor, staticProps);
|
||||||
|
return Constructor;
|
||||||
|
};
|
||||||
|
}();
|
||||||
|
|
||||||
|
TOOLTIP_PADDING_X = 30;
|
||||||
|
TOOLTIP_PADDING_Y = 10;
|
||||||
|
|
||||||
|
_export('AnnotationTooltip', AnnotationTooltip = function () {
|
||||||
|
function AnnotationTooltip(elem, scope) {
|
||||||
|
_classCallCheck(this, AnnotationTooltip);
|
||||||
|
|
||||||
|
this.scope = scope;
|
||||||
|
this.dashboard = scope.ctrl.dashboard;
|
||||||
|
this.panelCtrl = scope.ctrl;
|
||||||
|
this.panel = scope.ctrl.panel;
|
||||||
|
this.mouseOverAnnotationTick = false;
|
||||||
|
|
||||||
|
elem.on("mouseover", this.onMouseOver.bind(this));
|
||||||
|
elem.on("mouseleave", this.onMouseLeave.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_createClass(AnnotationTooltip, [{
|
||||||
|
key: 'onMouseOver',
|
||||||
|
value: function onMouseOver(e) {
|
||||||
|
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.tooltip) {
|
||||||
|
this.add();
|
||||||
|
this.move(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: 'onMouseLeave',
|
||||||
|
value: function onMouseLeave() {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: 'onMouseMove',
|
||||||
|
value: function onMouseMove(e) {
|
||||||
|
if (!this.panel.tooltip.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.move(e);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: 'add',
|
||||||
|
value: function add() {
|
||||||
|
this.tooltipBase = d3.select("body").append("div").attr("class", "statusmap-annotation-tooltip drop drop-popover drop-popover--annotation drop-element drop-enabled drop-target-attached-center drop-open drop-open-transitionend drop-after-open").style("position", "absolute");
|
||||||
|
this.tooltip = this.tooltipBase.append("div").attr("class", "drop-content").append("div").append("annotation-tooltip").append("div").attr("class", "graph-annotation");
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: 'destroy',
|
||||||
|
value: function destroy() {
|
||||||
|
if (this.tooltip) {
|
||||||
|
this.tooltip.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltip = null;
|
||||||
|
|
||||||
|
if (this.tooltipBase) {
|
||||||
|
this.tooltipBase.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltipBase = null;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: 'show',
|
||||||
|
value: function show(pos) {
|
||||||
|
if (!this.panel.tooltip.show || !this.tooltip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// shared tooltip mode
|
||||||
|
//if (pos.panelRelY) {
|
||||||
|
// return;
|
||||||
|
//}
|
||||||
|
|
||||||
|
var annoId = d3.select(pos.target).attr('annoId');
|
||||||
|
if (!annoId) {
|
||||||
|
this.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var anno = this.panelCtrl.annotations[annoId];
|
||||||
|
if (!anno) {
|
||||||
|
this.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var annoTitle = "";
|
||||||
|
|
||||||
|
var tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
var annoTime = this.dashboard.formatDate(anno.time, tooltipTimeFormat);
|
||||||
|
var annoText = anno.text;
|
||||||
|
var annoTags = [];
|
||||||
|
if (anno.tags) {
|
||||||
|
annoTags = _.map(anno.tags, function (t) {
|
||||||
|
return { "text": t, "backColor": "rgb(63, 43, 91)", "borderColor": "rgb(101, 81, 129)" };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var tooltipHtml = '<div class="graph-annotation__header">\n <span class="graph-annotation__title">' + annoTitle + '</span>\n <span class="graph-annotation__time">' + annoTime + '</span></div>\n <div class="graph-annotation__body">\n <div>' + annoText + '</div>\n ' + _.join(_.map(annoTags, function (t) {
|
||||||
|
return '<span class="label label-tag small" style="background-color: ' + t.backColor + '; border-color: ' + t.borderColor + '">' + t.text + '</span>';
|
||||||
|
}), "") + '\n </div>\n <div class="statusmap-histogram"></div>';
|
||||||
|
|
||||||
|
this.tooltip.html(tooltipHtml);
|
||||||
|
|
||||||
|
this.move(pos);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
key: 'move',
|
||||||
|
value: function move(pos) {
|
||||||
|
if (!this.tooltipBase) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elem = $(this.tooltipBase.node())[0];
|
||||||
|
var tooltipWidth = elem.clientWidth;
|
||||||
|
var tooltipHeight = elem.clientHeight;
|
||||||
|
|
||||||
|
var left = pos.pageX - tooltipWidth / 2;
|
||||||
|
var top = pos.pageY + TOOLTIP_PADDING_Y;
|
||||||
|
|
||||||
|
if (pos.pageX + tooltipWidth / 2 + 10 > window.innerWidth) {
|
||||||
|
left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos.pageY - window.pageYOffset + tooltipHeight + 20 > window.innerHeight) {
|
||||||
|
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tooltipBase.style("left", left + "px").style("top", top + "px");
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
return AnnotationTooltip;
|
||||||
|
}());
|
||||||
|
|
||||||
|
_export('AnnotationTooltip', AnnotationTooltip);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
//# sourceMappingURL=annotations.js.map
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic/index', 'app/core/core', 'app/core/utils/ticks'], function (_export, _context) {
|
System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic/index', 'app/core/core', 'app/core/utils/ticks'], function (_export, _context) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var angular, _, $, d3, d3ScaleChromatic, contextSrv, tickStep, mod, MIN_LEGEND_STEPS;
|
var angular, _, $, d3, d3ScaleChromatic, contextSrv, tickStep, mod, LEGEND_STEP_WIDTH;
|
||||||
|
|
||||||
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
|
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
|
||||||
var legendElem = $(elem).find('svg');
|
var legendElem = $(elem).find('svg');
|
||||||
@@ -13,24 +13,21 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
var legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
var legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
||||||
var legendHeight = legendElem.attr("height");
|
var legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
var rangeStep = 1;
|
var rangeStep = (rangeTo - rangeFrom) / (legendWidth / LEGEND_STEP_WIDTH);
|
||||||
if (rangeTo - rangeFrom > legendWidth) {
|
// width in pixels in legend space of unit segment in range space
|
||||||
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
// rangeStep * witdhFactor == width in pixels of one rangeStep
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
|
||||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
var widthFactor = legendWidth / (rangeTo - rangeFrom);
|
var widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
var colorScale = getColorScale(colorScheme, maxValue, minValue);
|
var colorScale = getColorScale(colorScheme, maxValue, minValue);
|
||||||
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect")
|
||||||
|
// translate from range space into pixels
|
||||||
|
// and shift all rectangles to the right by 10
|
||||||
|
.attr("x", function (d) {
|
||||||
return d * widthFactor + 10;
|
return d * widthFactor + 10;
|
||||||
}) // shift all color rectangles to the right
|
}).attr("y", 0)
|
||||||
.attr("y", 0).attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
|
// rectangles are slightly overlaped to prevent gaps
|
||||||
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
|
.attr("width", LEGEND_STEP_WIDTH + 1).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
|
||||||
return colorScale(d);
|
return colorScale(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,20 +42,21 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
var legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
var legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
||||||
var legendHeight = legendElem.attr("height");
|
var legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
var rangeStep = 10;
|
var rangeStep = (rangeTo - rangeFrom) / (legendWidth / LEGEND_STEP_WIDTH);
|
||||||
|
// width in pixels in legend space of unit segment in range space
|
||||||
|
// rangeStep * witdhFactor == width in pixels of one rangeStep
|
||||||
var widthFactor = legendWidth / (rangeTo - rangeFrom);
|
var widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
|
|
||||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
|
||||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
var opacityScale = getOpacityScale(options, maxValue, minValue);
|
var opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||||
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect")
|
||||||
|
// translate from range space into pixels
|
||||||
|
// and shift all rectangles to the right by 10
|
||||||
|
.attr("x", function (d) {
|
||||||
return d * widthFactor + 10;
|
return d * widthFactor + 10;
|
||||||
}) // shift all opacity rectangles to the right
|
}).attr("y", 0)
|
||||||
.attr("y", 0).attr("width", rangeStep * widthFactor).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
|
// rectangles are slightly overlaped to prevent gaps
|
||||||
|
.attr("width", LEGEND_STEP_WIDTH + 1).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
|
||||||
return opacityScale(d);
|
return opacityScale(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,13 +80,13 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
|
|
||||||
// calculate max width of tooltip and use it as width for each item
|
// calculate max width of tooltip and use it as width for each item
|
||||||
var textWidth = [];
|
var textWidth = [];
|
||||||
legend.selectAll(".hidden-texts").data(tooltips).enter().append("text").attr("class", "axis tick").attr("font-family", "sans-serif").text(function (d) {
|
legend.selectAll(".hidden-texts").data(tooltips).enter().append("text").attr("class", "axis tick hidden-texts").attr("font-family", "sans-serif").text(function (d) {
|
||||||
return d;
|
return d;
|
||||||
}).each(function (d, i) {
|
}).each(function (d, i) {
|
||||||
var thisWidth = this.getBBox().width;
|
var thisWidth = this.getBBox().width;
|
||||||
textWidth.push(thisWidth);
|
textWidth.push(thisWidth);
|
||||||
this.remove(); // remove them just after displaying them
|
|
||||||
});
|
});
|
||||||
|
legend.selectAll(".hidden-texts").remove();
|
||||||
|
|
||||||
var legendWidth = Math.floor(_.min([graphWidth - 30, (_.max(textWidth) + 3) * valuesNumber]));
|
var legendWidth = Math.floor(_.min([graphWidth - 30, (_.max(textWidth) + 3) * valuesNumber]));
|
||||||
legendElem.attr("width", legendWidth);
|
legendElem.attr("width", legendWidth);
|
||||||
@@ -180,22 +178,18 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
|
|
||||||
function drawSimpleColorLegend(elem, colorScale) {
|
function drawSimpleColorLegend(elem, colorScale) {
|
||||||
var legendElem = $(elem).find('svg');
|
var legendElem = $(elem).find('svg');
|
||||||
|
var legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
var legendWidth = Math.floor(legendElem.outerWidth());
|
var legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
var legendHeight = legendElem.attr("height");
|
var legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
if (legendWidth) {
|
if (legendWidth) {
|
||||||
var valuesNumber = Math.floor(legendWidth / 2);
|
var valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||||
var rangeStep = Math.floor(legendWidth / valuesNumber);
|
|
||||||
var valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
|
|
||||||
var legend = d3.select(legendElem.get(0));
|
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
||||||
var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange);
|
|
||||||
|
|
||||||
legendRects.enter().append("rect").attr("x", function (d) {
|
|
||||||
return d;
|
return d;
|
||||||
}).attr("y", 0).attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
|
}).attr("y", 0).attr("width", LEGEND_STEP_WIDTH + 1) // Overlap rectangles to prevent gaps
|
||||||
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
|
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
|
||||||
return colorScale(d);
|
return colorScale(d);
|
||||||
});
|
});
|
||||||
@@ -204,10 +198,9 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
|
|
||||||
function drawSimpleOpacityLegend(elem, options) {
|
function drawSimpleOpacityLegend(elem, options) {
|
||||||
var legendElem = $(elem).find('svg');
|
var legendElem = $(elem).find('svg');
|
||||||
var graphElem = $(elem);
|
var legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
var legend = d3.select(legendElem.get(0));
|
|
||||||
var legendWidth = Math.floor(legendElem.outerWidth());
|
var legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
var legendHeight = legendElem.attr("height");
|
var legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
@@ -219,13 +212,11 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
legendOpacityScale = d3.scalePow().exponent(options.exponent).domain([0, legendWidth]).range([0, 1]);
|
legendOpacityScale = d3.scalePow().exponent(options.exponent).domain([0, legendWidth]).range([0, 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var rangeStep = 10;
|
var valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||||
var valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange);
|
|
||||||
|
|
||||||
legendRects.enter().append("rect").attr("x", function (d) {
|
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
||||||
return d;
|
return d;
|
||||||
}).attr("y", 0).attr("width", rangeStep).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
|
}).attr("y", 0).attr("width", LEGEND_STEP_WIDTH + 1).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", function (d) {
|
||||||
return legendOpacityScale(d);
|
return legendOpacityScale(d);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -331,11 +322,11 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
}],
|
}],
|
||||||
execute: function () {
|
execute: function () {
|
||||||
mod = angular.module('grafana.directives');
|
mod = angular.module('grafana.directives');
|
||||||
MIN_LEGEND_STEPS = 10;
|
LEGEND_STEP_WIDTH = 2;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Color legend for heatmap editor.
|
* Bigger color legend for opacity and spectrum modes editor.
|
||||||
*/
|
*/
|
||||||
mod.directive('optionsColorLegend', function () {
|
mod.directive('optionsColorLegend', function () {
|
||||||
return {
|
return {
|
||||||
@@ -369,7 +360,7 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Heatmap legend with scale values.
|
* Graph legend with values.
|
||||||
*/
|
*/
|
||||||
mod.directive('statusHeatmapLegend', function () {
|
mod.directive('statusHeatmapLegend', function () {
|
||||||
return {
|
return {
|
||||||
@@ -386,6 +377,9 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
if (!ctrl.panel.legend.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
||||||
var rangeFrom = ctrl.cardsData.minValue;
|
var rangeFrom = ctrl.cardsData.minValue;
|
||||||
var rangeTo = ctrl.cardsData.maxValue;
|
var rangeTo = ctrl.cardsData.maxValue;
|
||||||
|
|||||||
@@ -101,9 +101,33 @@ System.register(["lodash"], function (_export, _context) {
|
|||||||
value: function getBucketColor(values) {
|
value: function getBucketColor(values) {
|
||||||
var thresholds = this.panel.color.thresholds;
|
var thresholds = this.panel.color.thresholds;
|
||||||
|
|
||||||
|
if (!values || values.length == 0) {
|
||||||
|
// treat as null value
|
||||||
|
return this.getMatchedThreshold(null).color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.length == 1) {
|
||||||
|
var threshold = this.getMatchedThreshold(values[0]);
|
||||||
|
if (!threshold || !threshold.color || threshold.color == "") {
|
||||||
|
return 'rgba(0,0,0,1)';
|
||||||
|
} else {
|
||||||
|
return threshold.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isAllValuesNulls = true;
|
||||||
|
for (var j = 0; j < values.length; j++) {
|
||||||
|
if (values[j] != null) {
|
||||||
|
isAllValuesNulls = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isAllValuesNulls) {
|
||||||
|
return this.getMatchedThreshold(null).color;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < thresholds.length; i++) {
|
for (var i = 0; i < thresholds.length; i++) {
|
||||||
for (var j = 0; j < values.length; j++) {
|
for (var _j = 0; _j < values.length; _j++) {
|
||||||
if (values[j] == thresholds[i].value) {
|
if (values[_j] == thresholds[i].value) {
|
||||||
return this.getDiscreteColor(i);
|
return this.getDiscreteColor(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +159,7 @@ System.register(["lodash"], function (_export, _context) {
|
|||||||
key: "getMatchedThreshold",
|
key: "getMatchedThreshold",
|
||||||
value: function getMatchedThreshold(value) {
|
value: function getMatchedThreshold(value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
if (this.panel.color.nullPointMode == 'as empty') {
|
if (this.panel.nullPointMode == 'as empty') {
|
||||||
// FIXME: make this explicit for user
|
// FIXME: make this explicit for user
|
||||||
// Right now this color never used because null as empty handles in getCardOpacity method.
|
// Right now this color never used because null as empty handles in getCardOpacity method.
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.status-heatmap-canvas-wrapper {
|
.status-heatmap-canvas-wrapper {
|
||||||
cursor: crosshair; }
|
cursor: crosshair; }
|
||||||
|
.status-heatmap-canvas-wrapper .datapoints-warning {
|
||||||
|
pointer-events: none; }
|
||||||
.status-heatmap-canvas-wrapper .datapoints-warning span {
|
.status-heatmap-canvas-wrapper .datapoints-warning span {
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
color: #d8d9da;
|
color: #d8d9da;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"mappings": "AAAA,8BAA+B;EAE7B,MAAM,EAAE,SAAS;EAEjB,uDAAyB;IACzB,gBAAgB,ECDL,OAAO;IDElB,KAAK,ECLM,OAAO;IDMlB,OAAO,EAAE,GAAG;;AAId,qBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAGhB,sCAAK;IACH,IAAI,ECfG,OAAO;IDgBd,KAAK,EChBE,OAAO;IDiBd,SAAS,EEjBE,IAAI;EFoBjB,sCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,ECrBM,OAAO;ED0BvB,yBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,8BAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,kBAAmB;EACjB,WAAW,EAAE,MAAM;EACnB,SAAS,EEvCI,IAAI;EFwCjB,gBAAgB,ECrCC,OAAO;EDsCxB,KAAK,ECxCM,OAAO;ED0ClB,iCAAe;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,GAAG;IACZ,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,8CAA8C;;AAI/D,yBAA0B;EACxB,IAAI,EClDY,OAAO;;ADsDvB,8BAAK;EACH,MAAM,EAAE,OAAgB;EACxB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAIlC,8BAA+B;EAC7B,MAAM,EAAE,MAAM;EAEd,kCAAI;IACF,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,MAAM;EAIrB,2DAA6B;IAC3B,UAAU,EAAE,GAAG;EAGjB,4DAA8B;IAC5B,OAAO,EAAE,YAAY;EAIrB,+CAAK;IACH,IAAI,ECxFG,OAAO;IDyFd,KAAK,ECzFE,OAAO;ID0Fd,SAAS,EE1FE,IAAI;EF6FjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EC9FM,OAAO;EDiGrB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,ECnGM,OAAO",
|
"mappings": "AAAA,8BAA+B;EAE7B,MAAM,EAAE,SAAS;EAGjB,kDAAoB;IAClB,cAAc,EAAE,IAAI;EAGtB,uDAAyB;IACzB,gBAAgB,ECNL,OAAO;IDOlB,KAAK,ECVM,OAAO;IDWlB,OAAO,EAAE,GAAG;;AAId,qBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAGhB,sCAAK;IACH,IAAI,ECpBG,OAAO;IDqBd,KAAK,ECrBE,OAAO;IDsBd,SAAS,EEtBE,IAAI;EFyBjB,sCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EC1BM,OAAO;ED+BvB,yBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,8BAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,kBAAmB;EACjB,WAAW,EAAE,MAAM;EACnB,SAAS,EE5CI,IAAI;EF6CjB,gBAAgB,EC1CC,OAAO;ED2CxB,KAAK,EC7CM,OAAO;ED+ClB,iCAAe;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,GAAG;IACZ,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,8CAA8C;;AAI/D,yBAA0B;EACxB,IAAI,ECvDY,OAAO;;AD2DvB,8BAAK;EACH,MAAM,EAAE,OAAgB;EACxB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAIlC,8BAA+B;EAC7B,MAAM,EAAE,MAAM;EAEd,kCAAI;IACF,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,MAAM;EAIrB,2DAA6B;IAC3B,UAAU,EAAE,GAAG;EAGjB,4DAA8B;IAC5B,OAAO,EAAE,YAAY;EAIrB,+CAAK;IACH,IAAI,EC7FG,OAAO;ID8Fd,KAAK,EC9FE,OAAO;ID+Fd,SAAS,EE/FE,IAAI;EFkGjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,ECnGM,OAAO;EDsGrB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,ECxGM,OAAO",
|
||||||
"sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.dark.scss","../../src/css/_variables.scss"],
|
"sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.dark.scss","../../src/css/_variables.scss"],
|
||||||
"names": [],
|
"names": [],
|
||||||
"file": "statusmap.dark.css"
|
"file": "statusmap.dark.css"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.status-heatmap-canvas-wrapper {
|
.status-heatmap-canvas-wrapper {
|
||||||
cursor: crosshair; }
|
cursor: crosshair; }
|
||||||
|
.status-heatmap-canvas-wrapper .datapoints-warning {
|
||||||
|
pointer-events: none; }
|
||||||
.status-heatmap-canvas-wrapper .datapoints-warning span {
|
.status-heatmap-canvas-wrapper .datapoints-warning span {
|
||||||
background-color: #e9edf2;
|
background-color: #e9edf2;
|
||||||
color: #52545c;
|
color: #52545c;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"mappings": "AAAA,8BAA+B;EAE7B,MAAM,EAAE,SAAS;EAEjB,uDAAyB;IACzB,gBAAgB,ECDL,OAAO;IDElB,KAAK,ECLM,OAAO;IDMlB,OAAO,EAAE,GAAG;;AAId,qBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAGhB,sCAAK;IACH,IAAI,ECfG,OAAO;IDgBd,KAAK,EChBE,OAAO;IDiBd,SAAS,EEjBE,IAAI;EFoBjB,sCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,ECrBM,OAAO;ED0BvB,yBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,8BAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,kBAAmB;EACjB,WAAW,EAAE,MAAM;EACnB,SAAS,EEvCI,IAAI;EFwCjB,gBAAgB,ECrCC,OAAO;EDsCxB,KAAK,ECxCM,OAAO;ED0ClB,iCAAe;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,GAAG;IACZ,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,8CAA8C;;AAI/D,yBAA0B;EACxB,IAAI,EClDY,OAAO;;ADsDvB,8BAAK;EACH,MAAM,EAAE,OAAgB;EACxB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAIlC,8BAA+B;EAC7B,MAAM,EAAE,MAAM;EAEd,kCAAI;IACF,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,MAAM;EAIrB,2DAA6B;IAC3B,UAAU,EAAE,GAAG;EAGjB,4DAA8B;IAC5B,OAAO,EAAE,YAAY;EAIrB,+CAAK;IACH,IAAI,ECxFG,OAAO;IDyFd,KAAK,ECzFE,OAAO;ID0Fd,SAAS,EE1FE,IAAI;EF6FjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EC9FM,OAAO;EDiGrB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,ECnGM,OAAO",
|
"mappings": "AAAA,8BAA+B;EAE7B,MAAM,EAAE,SAAS;EAGjB,kDAAoB;IAClB,cAAc,EAAE,IAAI;EAGtB,uDAAyB;IACzB,gBAAgB,ECNL,OAAO;IDOlB,KAAK,ECVM,OAAO;IDWlB,OAAO,EAAE,GAAG;;AAId,qBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAGhB,sCAAK;IACH,IAAI,ECpBG,OAAO;IDqBd,KAAK,ECrBE,OAAO;IDsBd,SAAS,EEtBE,IAAI;EFyBjB,sCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EC1BM,OAAO;ED+BvB,yBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,8BAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,kBAAmB;EACjB,WAAW,EAAE,MAAM;EACnB,SAAS,EE5CI,IAAI;EF6CjB,gBAAgB,EC1CC,OAAO;ED2CxB,KAAK,EC7CM,OAAO;ED+ClB,iCAAe;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,GAAG;IACZ,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,8CAA8C;;AAI/D,yBAA0B;EACxB,IAAI,ECvDY,OAAO;;AD2DvB,8BAAK;EACH,MAAM,EAAE,OAAgB;EACxB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAIlC,8BAA+B;EAC7B,MAAM,EAAE,MAAM;EAEd,kCAAI;IACF,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,MAAM;EAIrB,2DAA6B;IAC3B,UAAU,EAAE,GAAG;EAGjB,4DAA8B;IAC5B,OAAO,EAAE,YAAY;EAIrB,+CAAK;IACH,IAAI,EC7FG,OAAO;ID8Fd,KAAK,EC9FE,OAAO;ID+Fd,SAAS,EE/FE,IAAI;EFkGjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,ECnGM,OAAO;EDsGrB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,ECxGM,OAAO",
|
||||||
"sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.light.scss","../../src/css/_variables.scss"],
|
"sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.light.scss","../../src/css/_variables.scss"],
|
||||||
"names": [],
|
"names": [],
|
||||||
"file": "statusmap.light.css"
|
"file": "statusmap.light.css"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -1,7 +1,7 @@
|
|||||||
<div class="status-heatmap-wrapper">
|
<div class="status-heatmap-wrapper">
|
||||||
<div class="status-heatmap-canvas-wrapper">
|
<div class="status-heatmap-canvas-wrapper">
|
||||||
|
|
||||||
<div class="datapoints-warning">
|
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined">
|
||||||
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
|
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
|
||||||
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
|
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,40 +55,70 @@
|
|||||||
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
<div class="gf-form" ng-show="ctrl.panel.color.thresholds.length == 0">
|
||||||
|
<label class="gf-form-label width-2">0</label>
|
||||||
<label class="gf-form-label width-2">{{ $index }}</label>
|
<label class="gf-form-input width-30">No statuses defined. Add new status or use presets below.</label>
|
||||||
<label class="gf-form-label width-9" ng-if="ctrl.panel.useMax">If bucket has value</label>
|
|
||||||
<label class="gf-form-label width-9" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
|
|
||||||
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
|
|
||||||
<label class="gf-form-label">
|
|
||||||
fill
|
|
||||||
</label>
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
|
|
||||||
</label>
|
|
||||||
<label class="gf-form-label">set tooltip</label>
|
|
||||||
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
|
|
||||||
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<a class="pointer" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
|
||||||
<i class="fa fa-trash"/>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
||||||
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
|
<div class="gf-form">
|
||||||
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
|
<label class="gf-form-label width-2">{{ $index }}</label>
|
||||||
Add trafic lights
|
</div>
|
||||||
</a>
|
<div class="gf-form">
|
||||||
</label>
|
<label class="gf-form-label width-9 query-keyword" ng-if="ctrl.panel.useMax">If bucket has value</label>
|
||||||
<label class="gf-form-label">
|
<label class="gf-form-label width-9 query-keyword" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
|
||||||
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
|
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
|
||||||
Add new status
|
<label class="gf-form-label query-keyword">
|
||||||
</a>
|
fill
|
||||||
</label>
|
</label>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label query-keyword">set tooltip</label>
|
||||||
|
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
||||||
|
<i class="fa fa-trash"/>
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-inline">
|
||||||
|
<div class="gf-form"></div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn btn-inverse" ng-click="ctrl.onEditorAddThreshold()">
|
||||||
|
<i class="fa fa-plus"></i> Add new status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn btn-inverse" ng-click="ctrl.onEditorRemoveThresholds()">
|
||||||
|
<i class="fa fa-minus"></i> Remove all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-inline" ng-show="ctrl.panel.color.thresholds.length == 0">
|
||||||
|
<div class="gf-form"></div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">Presets</label>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn" ng-click="ctrl.onEditorAddThreeLights()">
|
||||||
|
Red-Yellow-Green
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn" ng-click="ctrl.onEditorAddSolarized()">
|
||||||
|
Solarized
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
||||||
@@ -118,7 +148,16 @@
|
|||||||
label="Show tooltip"
|
label="Show tooltip"
|
||||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-8">Y axis sort</label>
|
||||||
|
<div class="gf-form-select-wrapper">
|
||||||
|
<select class="gf-form-input max-width-8"
|
||||||
|
ng-model="ctrl.panel.yAxisSort"
|
||||||
|
ng-options="f for f in ['metrics', 'a → z', 'z → a']"
|
||||||
|
ng-change="ctrl.render()"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Bucket</h5>
|
<h5 class="section-heading">Bucket</h5>
|
||||||
@@ -142,12 +181,16 @@
|
|||||||
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Spacing</label>
|
<label class="gf-form-label width-9">V spacing</label>
|
||||||
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Card spacing in pixels'" ng-model="ctrl.panel.cards.cardSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards vertical spacing in pixels'" ng-model="ctrl.panel.cards.cardVSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-9">H spacing</label>
|
||||||
|
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards horizontal spacing in pixels'" ng-model="ctrl.panel.cards.cardHSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Rounding</label>
|
<label class="gf-form-label width-9">Rounding</label>
|
||||||
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Angles rounding in percent'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Cards rounding radius in pixels'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
{"name": "Color mapping", "path": "img/color-mapping.png"},
|
{"name": "Color mapping", "path": "img/color-mapping.png"},
|
||||||
{"name": "Queries example", "path": "img/queries-example.png"}
|
{"name": "Queries example", "path": "img/queries-example.png"}
|
||||||
],
|
],
|
||||||
"version": "0.0.2",
|
"version": "0.1.1",
|
||||||
"updated": "2018-10-17"
|
"updated": "2019-03-22"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/core', 'app/core/utils/ticks', 'd3', './libs/d3-scale-chromatic/index', './tooltip'], function (_export, _context) {
|
System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/core', 'app/core/utils/ticks', 'd3', './libs/d3-scale-chromatic/index', './tooltip', './annotations'], function (_export, _context) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var _, $, moment, kbn, appEvents, contextSrv, tickStep, getScaledDecimals, getFlotTickSize, d3, d3ScaleChromatic, StatusHeatmapTooltip, MIN_CARD_SIZE, CARD_SPACING, CARD_ROUND, DATA_RANGE_WIDING_FACTOR, DEFAULT_X_TICK_SIZE_PX, DEFAULT_Y_TICK_SIZE_PX, X_AXIS_TICK_PADDING, Y_AXIS_TICK_PADDING, MIN_SELECTION_WIDTH;
|
var _, $, moment, kbn, appEvents, contextSrv, tickStep, getScaledDecimals, getFlotTickSize, d3, d3ScaleChromatic, StatusHeatmapTooltip, AnnotationTooltip, MIN_CARD_SIZE, CARD_H_SPACING, CARD_V_SPACING, CARD_ROUND, DATA_RANGE_WIDING_FACTOR, DEFAULT_X_TICK_SIZE_PX, DEFAULT_Y_TICK_SIZE_PX, X_AXIS_TICK_PADDING, Y_AXIS_TICK_PADDING, MIN_SELECTION_WIDTH;
|
||||||
|
|
||||||
function link(scope, elem, attrs, ctrl) {
|
function link(scope, elem, attrs, ctrl) {
|
||||||
var data = void 0,
|
var data = void 0,
|
||||||
@@ -15,6 +15,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
// $heatmap is JQuery object, but heatmap is D3
|
// $heatmap is JQuery object, but heatmap is D3
|
||||||
var $heatmap = elem.find('.status-heatmap-panel');
|
var $heatmap = elem.find('.status-heatmap-panel');
|
||||||
var tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
var tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
||||||
|
var annotationTooltip = new AnnotationTooltip($heatmap, scope);
|
||||||
|
|
||||||
var width = void 0,
|
var width = void 0,
|
||||||
height = void 0,
|
height = void 0,
|
||||||
@@ -26,7 +27,8 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
chartBottom = void 0,
|
chartBottom = void 0,
|
||||||
yAxisWidth = void 0,
|
yAxisWidth = void 0,
|
||||||
xAxisHeight = void 0,
|
xAxisHeight = void 0,
|
||||||
cardSpacing = void 0,
|
cardVSpacing = void 0,
|
||||||
|
cardHSpacing = void 0,
|
||||||
cardRound = void 0,
|
cardRound = void 0,
|
||||||
cardWidth = void 0,
|
cardWidth = void 0,
|
||||||
cardHeight = void 0,
|
cardHeight = void 0,
|
||||||
@@ -77,7 +79,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
return text.getBBox().width;
|
return text.getBBox().width;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return max_text_width;
|
return Math.ceil(max_text_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getXAxisHeight(elem) {
|
function getXAxisHeight(elem) {
|
||||||
@@ -121,9 +123,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
function getYScale(ticks) {
|
function getYScale(ticks) {
|
||||||
var range = [];
|
var range = [];
|
||||||
var step = chartHeight / ticks.length;
|
var step = chartHeight / ticks.length;
|
||||||
range.push(chartHeight);
|
// svg has y=0 on the top, so top card should have a minimal value in range
|
||||||
|
range.push(step);
|
||||||
for (var i = 1; i < ticks.length; i++) {
|
for (var i = 1; i < ticks.length; i++) {
|
||||||
range.push(chartHeight - step * i);
|
range.push(step * (i + 1));
|
||||||
}
|
}
|
||||||
return d3.scaleOrdinal().domain(ticks).range(range);
|
return d3.scaleOrdinal().domain(ticks).range(range);
|
||||||
}
|
}
|
||||||
@@ -132,9 +135,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
function getYAxisScale(ticks) {
|
function getYAxisScale(ticks) {
|
||||||
var range = [];
|
var range = [];
|
||||||
var step = chartHeight / ticks.length;
|
var step = chartHeight / ticks.length;
|
||||||
range.push(chartHeight - yOffset);
|
// svg has y=0 on the top, so top tick should have a minimal value in range
|
||||||
|
range.push(yOffset);
|
||||||
for (var i = 1; i < ticks.length; i++) {
|
for (var i = 1; i < ticks.length; i++) {
|
||||||
range.push(chartHeight - step * i - yOffset);
|
range.push(step * i + yOffset);
|
||||||
}
|
}
|
||||||
return d3.scaleOrdinal().domain(ticks).range(range);
|
return d3.scaleOrdinal().domain(ticks).range(range);
|
||||||
}
|
}
|
||||||
@@ -149,6 +153,16 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
ticks = [''];
|
ticks = [''];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (panel.yAxisSort == 'a → z') {
|
||||||
|
ticks.sort(function (a, b) {
|
||||||
|
return a.localeCompare(b, 'en', { ignorePunctuation: false, numeric: true });
|
||||||
|
});
|
||||||
|
} else if (panel.yAxisSort == 'z → a') {
|
||||||
|
ticks.sort(function (b, a) {
|
||||||
|
return a.localeCompare(b, 'en', { ignorePunctuation: false, numeric: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var yAxisScale = getYAxisScale(ticks);
|
var yAxisScale = getYAxisScale(ticks);
|
||||||
scope.yScale = yScale = getYScale(ticks);
|
scope.yScale = yScale = getYScale(ticks);
|
||||||
|
|
||||||
@@ -216,12 +230,13 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
chartTop = margin.top;
|
chartTop = margin.top;
|
||||||
chartBottom = chartTop + chartHeight;
|
chartBottom = chartTop + chartHeight;
|
||||||
|
|
||||||
cardSpacing = panel.cards.cardSpacing !== null ? panel.cards.cardSpacing : CARD_SPACING;
|
cardHSpacing = panel.cards.cardHSpacing !== null ? panel.cards.cardHSpacing : CARD_H_SPACING;
|
||||||
|
cardVSpacing = panel.cards.cardVSpacing !== null ? panel.cards.cardVSpacing : CARD_V_SPACING;
|
||||||
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
|
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
|
||||||
|
|
||||||
// calculate yOffset for YAxis
|
// calculate yOffset for YAxis
|
||||||
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
||||||
cardHeight = yGridSize ? yGridSize - cardSpacing : 0;
|
cardHeight = yGridSize ? yGridSize - cardVSpacing : 0;
|
||||||
yOffset = cardHeight / 2;
|
yOffset = cardHeight / 2;
|
||||||
|
|
||||||
addYAxis();
|
addYAxis();
|
||||||
@@ -229,9 +244,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||||
chartWidth = width - yAxisWidth - margin.right;
|
chartWidth = width - yAxisWidth - margin.right;
|
||||||
|
|
||||||
|
// TODO allow per-y cardWidth!
|
||||||
// we need to fill chartWidth with xBucketSize cards.
|
// we need to fill chartWidth with xBucketSize cards.
|
||||||
xGridSize = chartWidth / (cardsData.xBucketSize + 1);
|
xGridSize = chartWidth / (cardsData.xBucketSize + 1);
|
||||||
cardWidth = xGridSize - cardSpacing;
|
cardWidth = xGridSize - cardHSpacing;
|
||||||
|
|
||||||
addXAxis();
|
addXAxis();
|
||||||
xAxisHeight = getXAxisHeight(heatmap);
|
xAxisHeight = getXAxisHeight(heatmap);
|
||||||
@@ -274,6 +290,8 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
resetCardHighLight(event);
|
resetCardHighLight(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_renderAnnotations();
|
||||||
|
|
||||||
ctrl.events.emit('render-complete', {
|
ctrl.events.emit('render-complete', {
|
||||||
"chartWidth": chartWidth
|
"chartWidth": chartWidth
|
||||||
});
|
});
|
||||||
@@ -321,7 +339,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
var cx = xScale(d.x);
|
var cx = xScale(d.x);
|
||||||
|
|
||||||
if (cx - cardWidth / 2 < 0) {
|
if (cx - cardWidth / 2 < 0) {
|
||||||
x = yAxisWidth + cardSpacing / 2;
|
x = yAxisWidth + cardHSpacing / 2;
|
||||||
} else {
|
} else {
|
||||||
x = yAxisWidth + cx - cardWidth / 2;
|
x = yAxisWidth + cx - cardWidth / 2;
|
||||||
}
|
}
|
||||||
@@ -337,11 +355,11 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
if (cx < cardWidth / 2) {
|
if (cx < cardWidth / 2) {
|
||||||
// Center should not exceed half of card.
|
// Center should not exceed half of card.
|
||||||
// Cut card to the left to prevent overlay of y axis.
|
// Cut card to the left to prevent overlay of y axis.
|
||||||
var cutted_width = cx - cardSpacing / 2 + cardWidth / 2;
|
var cutted_width = cx - cardHSpacing / 2 + cardWidth / 2;
|
||||||
w = cutted_width > 0 ? cutted_width : 0;
|
w = cutted_width > 0 ? cutted_width : 0;
|
||||||
} else if (chartWidth - cx < cardWidth / 2) {
|
} else if (chartWidth - cx < cardWidth / 2) {
|
||||||
// Cut card to the right to prevent overlay of right graph edge.
|
// Cut card to the right to prevent overlay of right graph edge.
|
||||||
w = cardWidth / 2 + (chartWidth - cx - cardSpacing / 2);
|
w = cardWidth / 2 + (chartWidth - cx - cardHSpacing / 2);
|
||||||
} else {
|
} else {
|
||||||
w = cardWidth;
|
w = cardWidth;
|
||||||
}
|
}
|
||||||
@@ -349,21 +367,25 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
// Card width should be MIN_CARD_SIZE at least
|
// Card width should be MIN_CARD_SIZE at least
|
||||||
w = Math.max(w, MIN_CARD_SIZE);
|
w = Math.max(w, MIN_CARD_SIZE);
|
||||||
|
|
||||||
|
if (cardHSpacing == 0) {
|
||||||
|
w = w + 1;
|
||||||
|
}
|
||||||
|
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardY(d) {
|
function getCardY(d) {
|
||||||
return yScale(d.y) + chartTop - cardHeight - cardSpacing / 2;
|
return yScale(d.y) + chartTop - cardHeight - cardVSpacing / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardHeight(d) {
|
function getCardHeight(d) {
|
||||||
var ys = yScale(d.y);
|
var ys = yScale(d.y);
|
||||||
var y = ys + chartTop - cardHeight - cardSpacing / 2;
|
var y = ys + chartTop - cardHeight - cardVSpacing / 2;
|
||||||
var h = cardHeight;
|
var h = cardHeight;
|
||||||
|
|
||||||
// Cut card height to prevent overlay
|
// Cut card height to prevent overlay
|
||||||
if (y < chartTop) {
|
if (y < chartTop) {
|
||||||
h = ys - cardSpacing / 2;
|
h = ys - cardVSpacing / 2;
|
||||||
} else if (ys > chartBottom) {
|
} else if (ys > chartBottom) {
|
||||||
h = chartBottom - y;
|
h = chartBottom - y;
|
||||||
} else if (y + cardHeight > chartBottom) {
|
} else if (y + cardHeight > chartBottom) {
|
||||||
@@ -375,6 +397,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
// Card height should be MIN_CARD_SIZE at least
|
// Card height should be MIN_CARD_SIZE at least
|
||||||
h = Math.max(h, MIN_CARD_SIZE);
|
h = Math.max(h, MIN_CARD_SIZE);
|
||||||
|
|
||||||
|
if (cardVSpacing == 0) {
|
||||||
|
h = h + 1;
|
||||||
|
}
|
||||||
|
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,6 +478,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
function onMouseLeave() {
|
function onMouseLeave() {
|
||||||
appEvents.emit('graph-hover-clear');
|
appEvents.emit('graph-hover-clear');
|
||||||
clearCrosshair();
|
clearCrosshair();
|
||||||
|
//annotationTooltip.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove(event) {
|
function onMouseMove(event) {
|
||||||
@@ -463,6 +490,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
// Clear crosshair and tooltip
|
// Clear crosshair and tooltip
|
||||||
clearCrosshair();
|
clearCrosshair();
|
||||||
tooltip.destroy();
|
tooltip.destroy();
|
||||||
|
annotationTooltip.destroy();
|
||||||
|
|
||||||
selection.x2 = limitSelection(event.offsetX);
|
selection.x2 = limitSelection(event.offsetX);
|
||||||
drawSelection(selection.x1, selection.x2);
|
drawSelection(selection.x1, selection.x2);
|
||||||
@@ -470,6 +498,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
emitGraphHoverEvet(event);
|
emitGraphHoverEvet(event);
|
||||||
drawCrosshair(event.offsetX);
|
drawCrosshair(event.offsetX);
|
||||||
tooltip.show(event); //, data);
|
tooltip.show(event); //, data);
|
||||||
|
annotationTooltip.show(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,6 +601,49 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
$heatmap.on("mousedown", onMouseDown);
|
$heatmap.on("mousedown", onMouseDown);
|
||||||
$heatmap.on("mousemove", onMouseMove);
|
$heatmap.on("mousemove", onMouseMove);
|
||||||
$heatmap.on("mouseleave", onMouseLeave);
|
$heatmap.on("mouseleave", onMouseLeave);
|
||||||
|
|
||||||
|
function _renderAnnotations() {
|
||||||
|
if (!ctrl.annotations || ctrl.annotations.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!heatmap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var annoData = _.map(ctrl.annotations, function (d, i) {
|
||||||
|
return { "x": Math.floor(yAxisWidth + xScale(d.time)), "id": i, "anno": d.source };
|
||||||
|
});
|
||||||
|
|
||||||
|
var anno = heatmap.append("g").attr("class", "statusmap-annotations").attr("transform", "translate(0.5,0)").selectAll(".statusmap-annotations").data(annoData).enter().append("g");
|
||||||
|
anno.append("line")
|
||||||
|
//.attr("class", "statusmap-annotation-tick")
|
||||||
|
.attr("x1", function (d) {
|
||||||
|
return d.x;
|
||||||
|
}).attr("y1", chartTop).attr("x2", function (d) {
|
||||||
|
return d.x;
|
||||||
|
}).attr("y2", chartBottom).style("stroke", function (d) {
|
||||||
|
return d.anno.iconColor;
|
||||||
|
}).style("stroke-width", 1).style("stroke-dasharray", "3,3");
|
||||||
|
anno.append("polygon").attr("points", function (d) {
|
||||||
|
return [[d.x, chartBottom + 1], [d.x - 5, chartBottom + 6], [d.x + 5, chartBottom + 6]].join(" ");
|
||||||
|
}).style("stroke-width", 0).style("fill", function (d) {
|
||||||
|
return d.anno.iconColor;
|
||||||
|
});
|
||||||
|
// Polygons didn't fire mouseevents
|
||||||
|
anno.append("rect").attr("x", function (d) {
|
||||||
|
return d.x - 5;
|
||||||
|
}).attr("width", 10).attr("y", chartBottom + 1).attr("height", 5).attr("class", "statusmap-annotation-tick").attr("annoId", function (d) {
|
||||||
|
return d.id;
|
||||||
|
}).style("opacity", 0);
|
||||||
|
|
||||||
|
var $ticks = $heatmap.find(".statusmap-annotation-tick");
|
||||||
|
$ticks.on("mouseenter", function (event) {
|
||||||
|
annotationTooltip.mouseOverAnnotationTick = true;
|
||||||
|
}).on("mouseleave", function (event) {
|
||||||
|
annotationTooltip.mouseOverAnnotationTick = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_export('default', link);
|
_export('default', link);
|
||||||
@@ -622,10 +694,13 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
|||||||
d3ScaleChromatic = _libsD3ScaleChromaticIndex;
|
d3ScaleChromatic = _libsD3ScaleChromaticIndex;
|
||||||
}, function (_tooltip) {
|
}, function (_tooltip) {
|
||||||
StatusHeatmapTooltip = _tooltip.StatusHeatmapTooltip;
|
StatusHeatmapTooltip = _tooltip.StatusHeatmapTooltip;
|
||||||
|
}, function (_annotations) {
|
||||||
|
AnnotationTooltip = _annotations.AnnotationTooltip;
|
||||||
}],
|
}],
|
||||||
execute: function () {
|
execute: function () {
|
||||||
MIN_CARD_SIZE = 5;
|
MIN_CARD_SIZE = 5;
|
||||||
CARD_SPACING = 2;
|
CARD_H_SPACING = 2;
|
||||||
|
CARD_V_SPACING = 2;
|
||||||
CARD_ROUND = 0;
|
CARD_ROUND = 0;
|
||||||
DATA_RANGE_WIDING_FACTOR = 1.2;
|
DATA_RANGE_WIDING_FACTOR = 1.2;
|
||||||
DEFAULT_X_TICK_SIZE_PX = 100;
|
DEFAULT_X_TICK_SIZE_PX = 100;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/kbn', './color_legend', './rendering', './options_editor', './color_mode_discrete'], function (_export, _context) {
|
System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/kbn', './color_legend', './rendering', './options_editor', './color_mode_discrete'], function (_export, _context) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var MetricsPanelCtrl, _, contextSrv, kbn, rendering, statusHeatmapOptionsEditor, ColorModeDiscrete, CANVAS, SVG, VALUE_INDEX, TIME_INDEX, panelDefaults, renderer, colorSchemes, colorModes, opacityScales, StatusHeatmapCtrl;
|
var MetricsPanelCtrl, _, contextSrv, kbn, rendering, statusHeatmapOptionsEditor, ColorModeDiscrete, _get, CANVAS, SVG, VALUE_INDEX, TIME_INDEX, panelDefaults, renderer, colorSchemes, colorModes, opacityScales, StatusHeatmapCtrl;
|
||||||
|
|
||||||
function _classCallCheck(instance, Constructor) {
|
function _classCallCheck(instance, Constructor) {
|
||||||
if (!(instance instanceof Constructor)) {
|
if (!(instance instanceof Constructor)) {
|
||||||
@@ -52,6 +52,31 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
ColorModeDiscrete = _color_mode_discrete.ColorModeDiscrete;
|
ColorModeDiscrete = _color_mode_discrete.ColorModeDiscrete;
|
||||||
}],
|
}],
|
||||||
execute: function () {
|
execute: function () {
|
||||||
|
_get = function get(object, property, receiver) {
|
||||||
|
if (object === null) object = Function.prototype;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(object, property);
|
||||||
|
|
||||||
|
if (desc === undefined) {
|
||||||
|
var parent = Object.getPrototypeOf(object);
|
||||||
|
|
||||||
|
if (parent === null) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return get(parent, property, receiver);
|
||||||
|
}
|
||||||
|
} else if ("value" in desc) {
|
||||||
|
return desc.value;
|
||||||
|
} else {
|
||||||
|
var getter = desc.get;
|
||||||
|
|
||||||
|
if (getter === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getter.call(receiver);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
CANVAS = 'CANVAS';
|
CANVAS = 'CANVAS';
|
||||||
SVG = 'SVG';
|
SVG = 'SVG';
|
||||||
VALUE_INDEX = 0;
|
VALUE_INDEX = 0;
|
||||||
@@ -71,7 +96,8 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
cardMinWidth: 5,
|
cardMinWidth: 5,
|
||||||
cardSpacing: 2,
|
cardVSpacing: 2,
|
||||||
|
cardHSpacing: 2,
|
||||||
cardRound: null
|
cardRound: null
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@@ -97,6 +123,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
},
|
},
|
||||||
// how null points should be handled
|
// how null points should be handled
|
||||||
nullPointMode: 'as empty',
|
nullPointMode: 'as empty',
|
||||||
|
yAxisSort: 'metrics',
|
||||||
highlightCards: true,
|
highlightCards: true,
|
||||||
useMax: true
|
useMax: true
|
||||||
};
|
};
|
||||||
@@ -116,7 +143,8 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
_export('StatusHeatmapCtrl', StatusHeatmapCtrl = function (_MetricsPanelCtrl) {
|
_export('StatusHeatmapCtrl', StatusHeatmapCtrl = function (_MetricsPanelCtrl) {
|
||||||
_inherits(StatusHeatmapCtrl, _MetricsPanelCtrl);
|
_inherits(StatusHeatmapCtrl, _MetricsPanelCtrl);
|
||||||
|
|
||||||
function StatusHeatmapCtrl($scope, $injector, $rootScope, timeSrv) {
|
/** @ngInject */
|
||||||
|
function StatusHeatmapCtrl($scope, $injector, $rootScope, timeSrv, annotationsSrv) {
|
||||||
_classCallCheck(this, StatusHeatmapCtrl);
|
_classCallCheck(this, StatusHeatmapCtrl);
|
||||||
|
|
||||||
var _this = _possibleConstructorReturn(this, (StatusHeatmapCtrl.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl)).call(this, $scope, $injector));
|
var _this = _possibleConstructorReturn(this, (StatusHeatmapCtrl.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl)).call(this, $scope, $injector));
|
||||||
@@ -132,7 +160,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
var chartWidth = _.max([panelWidth - 200, panelWidth / 2]);
|
var chartWidth = _.max([panelWidth - 200, panelWidth / 2]);
|
||||||
|
|
||||||
var minCardWidth = _this.panel.cards.cardMinWidth;
|
var minCardWidth = _this.panel.cards.cardMinWidth;
|
||||||
var minSpacing = _this.panel.cards.cardSpacing;
|
var minSpacing = _this.panel.cards.cardHSpacing;
|
||||||
var maxCardsCount = Math.ceil((chartWidth - minCardWidth) / (minCardWidth + minSpacing));
|
var maxCardsCount = Math.ceil((chartWidth - minCardWidth) / (minCardWidth + minSpacing));
|
||||||
|
|
||||||
var intervalMs = void 0;
|
var intervalMs = void 0;
|
||||||
@@ -167,11 +195,49 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
_this.interval = kbn.secondsToHms(intervalMs / 1000);
|
_this.interval = kbn.secondsToHms(intervalMs / 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_this.issueQueries = function (datasource) {
|
||||||
|
_this.annotationsPromise = _this.annotationsSrv.getAnnotations({
|
||||||
|
dashboard: _this.dashboard,
|
||||||
|
panel: _this.panel,
|
||||||
|
range: _this.range
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Wait for annotationSrv requests to get datasources to
|
||||||
|
* resolve before issuing queries. This allows the annotations
|
||||||
|
* service to fire annotations queries before graph queries
|
||||||
|
* (but not wait for completion). This resolves
|
||||||
|
* issue 11806.
|
||||||
|
*/
|
||||||
|
// 5.x before 5.4 doesn't have dataPromises
|
||||||
|
if ("undefined" !== typeof _this.annotationsSrv.datasourcePromises) {
|
||||||
|
return _this.annotationsSrv.datasourcePromises.then(function (r) {
|
||||||
|
return _get(StatusHeatmapCtrl.prototype.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl.prototype), 'issueQueries', _this).call(_this, datasource);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _get(StatusHeatmapCtrl.prototype.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl.prototype), 'issueQueries', _this).call(_this, datasource);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_this.onDataReceived = function (dataList) {
|
_this.onDataReceived = function (dataList) {
|
||||||
_this.data = dataList;
|
_this.data = dataList;
|
||||||
_this.cardsData = _this.convertToCards(_this.data);
|
_this.cardsData = _this.convertToCards(_this.data);
|
||||||
|
|
||||||
_this.render();
|
_this.annotationsPromise.then(function (result) {
|
||||||
|
_this.loading = false;
|
||||||
|
//this.alertState = result.alertState;
|
||||||
|
if (result.annotations && result.annotations.length > 0) {
|
||||||
|
_this.annotations = result.annotations;
|
||||||
|
} else {
|
||||||
|
_this.annotations = null;
|
||||||
|
}
|
||||||
|
_this.render();
|
||||||
|
}, function () {
|
||||||
|
_this.loading = false;
|
||||||
|
_this.annotations = null;
|
||||||
|
_this.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
//this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
_this.onInitEditMode = function () {
|
_this.onInitEditMode = function () {
|
||||||
@@ -207,6 +273,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
|
|
||||||
_this.onDataError = function () {
|
_this.onDataError = function () {
|
||||||
_this.data = [];
|
_this.data = [];
|
||||||
|
_this.annotations = [];
|
||||||
_this.render();
|
_this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -224,6 +291,11 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
_this.render();
|
_this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_this.onEditorRemoveThresholds = function () {
|
||||||
|
_this.panel.color.thresholds = [];
|
||||||
|
_this.render();
|
||||||
|
};
|
||||||
|
|
||||||
_this.onEditorAddThreeLights = function () {
|
_this.onEditorAddThreeLights = function () {
|
||||||
_this.panel.color.thresholds.push({ color: "red", value: 2, tooltip: "error" });
|
_this.panel.color.thresholds.push({ color: "red", value: 2, tooltip: "error" });
|
||||||
_this.panel.color.thresholds.push({ color: "yellow", value: 1, tooltip: "warning" });
|
_this.panel.color.thresholds.push({ color: "yellow", value: 1, tooltip: "warning" });
|
||||||
@@ -231,6 +303,18 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
_this.render();
|
_this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_this.onEditorAddSolarized = function () {
|
||||||
|
_this.panel.color.thresholds.push({ color: "#b58900", value: 0, tooltip: "yellow" });
|
||||||
|
_this.panel.color.thresholds.push({ color: "#cb4b16", value: 1, tooltip: "orange" });
|
||||||
|
_this.panel.color.thresholds.push({ color: "#dc322f", value: 2, tooltip: "red" });
|
||||||
|
_this.panel.color.thresholds.push({ color: "#d33682", value: 3, tooltip: "magenta" });
|
||||||
|
_this.panel.color.thresholds.push({ color: "#6c71c4", value: 4, tooltip: "violet" });
|
||||||
|
_this.panel.color.thresholds.push({ color: "#268bd2", value: 5, tooltip: "blue" });
|
||||||
|
_this.panel.color.thresholds.push({ color: "#2aa198", value: 6, tooltip: "cyan" });
|
||||||
|
_this.panel.color.thresholds.push({ color: "#859900", value: 7, tooltip: "green" });
|
||||||
|
_this.render();
|
||||||
|
};
|
||||||
|
|
||||||
_this.link = function (scope, elem, attrs, ctrl) {
|
_this.link = function (scope, elem, attrs, ctrl) {
|
||||||
rendering(scope, elem, attrs, ctrl);
|
rendering(scope, elem, attrs, ctrl);
|
||||||
};
|
};
|
||||||
@@ -243,51 +327,54 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
maxValue: 0,
|
maxValue: 0,
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
multipleValues: false,
|
multipleValues: false,
|
||||||
noColorDefined: false
|
noColorDefined: false,
|
||||||
|
targets: [], // array of available unique targets
|
||||||
|
targetIndex: {} // indices in data array for each of available unique targets
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data || data.length == 0) {
|
if (!data || data.length == 0) {
|
||||||
return cardsData;
|
return cardsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect uniq targets and their indexes in data array
|
// Collect uniq timestamps from data and spread over targets and timestamps
|
||||||
cardsData.targetIndex = {};
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
// collect uniq targets and their indices
|
||||||
var ts = data[i];
|
_.map(data, function (d, i) {
|
||||||
var target = ts.target;
|
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i);
|
||||||
if (cardsData.targetIndex[target] == undefined) {
|
});
|
||||||
cardsData.targetIndex[target] = [];
|
|
||||||
}
|
|
||||||
cardsData.targetIndex[target].push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO add some logic for targets heirarchy
|
// TODO add some logic for targets heirarchy
|
||||||
cardsData.targets = _.keys(cardsData.targetIndex);
|
cardsData.targets = _.keys(cardsData.targetIndex);
|
||||||
cardsData.targets.sort();
|
|
||||||
cardsData.yBucketSize = cardsData.targets.length;
|
cardsData.yBucketSize = cardsData.targets.length;
|
||||||
cardsData.xBucketSize = _.min(_.map(data, function (d) {
|
// Maximum number of buckets over x axis
|
||||||
|
cardsData.xBucketSize = _.max(_.map(data, function (d) {
|
||||||
return d.datapoints.length;
|
return d.datapoints.length;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Collect all values for each bucket from datapoints with similar target.
|
// Collect all values for each bucket from datapoints with similar target.
|
||||||
for (var _i = 0; _i < cardsData.targets.length; _i++) {
|
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
|
||||||
var _target = cardsData.targets[_i];
|
for (var i = 0; i < cardsData.targets.length; i++) {
|
||||||
|
var target = cardsData.targets[i];
|
||||||
|
|
||||||
for (var j = 0; j < cardsData.xBucketSize; j++) {
|
for (var j = 0; j < cardsData.xBucketSize; j++) {
|
||||||
var card = {
|
var card = {
|
||||||
id: _i * cardsData.xBucketSize + j,
|
id: i * cardsData.xBucketSize + j,
|
||||||
values: [],
|
values: [],
|
||||||
multipleValues: false,
|
multipleValues: false,
|
||||||
noColorDefined: false
|
noColorDefined: false,
|
||||||
|
y: target,
|
||||||
|
x: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
// collect values from all timeseries with target
|
// collect values from all timeseries with target
|
||||||
for (var si = 0; si < cardsData.targetIndex[_target].length; si++) {
|
for (var si = 0; si < cardsData.targetIndex[target].length; si++) {
|
||||||
var s = data[cardsData.targetIndex[_target][si]];
|
var s = data[cardsData.targetIndex[target][si]];
|
||||||
|
if (s.datapoints.length <= j) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
var datapoint = s.datapoints[j];
|
var datapoint = s.datapoints[j];
|
||||||
if (card.values.length === 0) {
|
if (card.values.length === 0) {
|
||||||
card.x = datapoint[TIME_INDEX];
|
card.x = datapoint[TIME_INDEX];
|
||||||
card.y = s.target;
|
|
||||||
}
|
}
|
||||||
card.values.push(datapoint[VALUE_INDEX]);
|
card.values.push(datapoint[VALUE_INDEX]);
|
||||||
}
|
}
|
||||||
@@ -305,7 +392,9 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
|
|
||||||
if (cardsData.minValue > card.minValue) cardsData.minValue = card.minValue;
|
if (cardsData.minValue > card.minValue) cardsData.minValue = card.minValue;
|
||||||
|
|
||||||
cardsData.cards.push(card);
|
if (card.x != -1) {
|
||||||
|
cardsData.cards.push(card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +428,9 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_this.annotations = [];
|
||||||
|
_this.annotationsSrv = annotationsSrv;
|
||||||
|
|
||||||
_this.events.on('data-received', _this.onDataReceived);
|
_this.events.on('data-received', _this.onDataReceived);
|
||||||
_this.events.on('data-snapshot-load', _this.onDataReceived);
|
_this.events.on('data-snapshot-load', _this.onDataReceived);
|
||||||
_this.events.on('data-error', _this.onDataError);
|
_this.events.on('data-error', _this.onDataError);
|
||||||
@@ -353,6 +445,9 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
|||||||
// override calculateInterval for discrete color mode
|
// override calculateInterval for discrete color mode
|
||||||
|
|
||||||
|
|
||||||
|
/* https://ethanschoonover.com/solarized/ */
|
||||||
|
|
||||||
|
|
||||||
// group values into buckets by target
|
// group values into buckets by target
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 8,
|
||||||
|
"iteration": 1545919919137,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"cards": {
|
||||||
|
"cardHSpacing": 2,
|
||||||
|
"cardMinWidth": 5,
|
||||||
|
"cardRound": null,
|
||||||
|
"cardVSpacing": 2
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"cardColor": "#b4ff00",
|
||||||
|
"colorScale": "sqrt",
|
||||||
|
"colorScheme": "interpolateOranges",
|
||||||
|
"defaultColor": "#757575",
|
||||||
|
"exponent": 0.5,
|
||||||
|
"max": 5,
|
||||||
|
"min": 1,
|
||||||
|
"mode": "discrete",
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"color": "#cca300",
|
||||||
|
"tooltip": "Pending",
|
||||||
|
"value": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#9ac48a",
|
||||||
|
"tooltip": "Running",
|
||||||
|
"value": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#bf1b00",
|
||||||
|
"tooltip": "Failed",
|
||||||
|
"value": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#3f6833",
|
||||||
|
"tooltip": "Succeeded",
|
||||||
|
"value": "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#f9e2d2",
|
||||||
|
"tooltip": "Unknown",
|
||||||
|
"value": "5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"decimals": null,
|
||||||
|
"unitFormat": "short"
|
||||||
|
},
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 16,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"highlightCards": true,
|
||||||
|
"id": 2,
|
||||||
|
"interval": "",
|
||||||
|
"legend": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "as empty",
|
||||||
|
"repeat": null,
|
||||||
|
"repeatDirection": null,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "kube_pod_status_phase{namespace=\"$namespace\", phase=\"Pending\"} == 1",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"instant": false,
|
||||||
|
"interval": "",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }}",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Running\"} == 1) * 2",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }}",
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Failed\"} == 1) * 3",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }}",
|
||||||
|
"refId": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Succeeded\"} == 1) * 4",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }}",
|
||||||
|
"refId": "D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Unknown\"} == 1) * 5",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }}",
|
||||||
|
"refId": "E"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Pod Status",
|
||||||
|
"tooltip": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"transparent": true,
|
||||||
|
"type": "flant-statusmap-panel",
|
||||||
|
"useMax": true,
|
||||||
|
"xAxis": {
|
||||||
|
"labelFormat": "%a %m/%d",
|
||||||
|
"minBucketWidthToShowWeekends": 4,
|
||||||
|
"show": true,
|
||||||
|
"showCrosshair": true,
|
||||||
|
"showWeekends": true
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"show": true,
|
||||||
|
"showCrosshair": false
|
||||||
|
},
|
||||||
|
"yAxisSort": "a → z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cards": {
|
||||||
|
"cardHSpacing": 2,
|
||||||
|
"cardMinWidth": 5,
|
||||||
|
"cardRound": null,
|
||||||
|
"cardVSpacing": 2
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"cardColor": "#b4ff00",
|
||||||
|
"colorScale": "sqrt",
|
||||||
|
"colorScheme": "interpolateOranges",
|
||||||
|
"defaultColor": "#757575",
|
||||||
|
"exponent": 0.5,
|
||||||
|
"max": 5,
|
||||||
|
"min": 1,
|
||||||
|
"mode": "discrete",
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"color": "#cca300",
|
||||||
|
"tooltip": "ContainerCreating",
|
||||||
|
"value": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#890f02",
|
||||||
|
"tooltip": "CrashLoopBackOff",
|
||||||
|
"value": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#bf1b00",
|
||||||
|
"tooltip": "ErrImagePull",
|
||||||
|
"value": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#58140c",
|
||||||
|
"tooltip": "ImagePullBackOff",
|
||||||
|
"value": "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#7eb26d",
|
||||||
|
"tooltip": "Running",
|
||||||
|
"value": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#052b51",
|
||||||
|
"tooltip": "Completed",
|
||||||
|
"value": "6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#c15c17",
|
||||||
|
"tooltip": "ContainerCannotRun",
|
||||||
|
"value": "7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#f9934e",
|
||||||
|
"tooltip": "Error",
|
||||||
|
"value": "8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "#eab839",
|
||||||
|
"tooltip": "OOMKilled",
|
||||||
|
"value": "9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"decimals": null,
|
||||||
|
"unitFormat": "short"
|
||||||
|
},
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"gridPos": {
|
||||||
|
"h": 16,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 16
|
||||||
|
},
|
||||||
|
"highlightCards": true,
|
||||||
|
"id": 3,
|
||||||
|
"interval": "",
|
||||||
|
"legend": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "as empty",
|
||||||
|
"repeatDirection": null,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "kube_pod_container_status_waiting_reason{namespace=\"$namespace\", reason=\"ContainerCreating\"} == 1",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"instant": false,
|
||||||
|
"interval": "",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_container_status_waiting_reason{namespace=\"$namespace\", reason=\"CrashLoopBackOff\"} == 1) * 2",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_container_status_waiting_reason{namespace=\"$namespace\", reason=\"ErrImagePull\"} == 1) * 3",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_container_status_waiting_reason{namespace=\"$namespace\", reason=\"ImagePullBackOff\"} == 1) * 4",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "D"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_container_status_running{namespace=\"$namespace\"} == 1) * 5",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_container_status_terminated_reason{namespace=\"$namespace\", reason=\"Completed\"} == 1) * 6",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "F"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_container_status_terminated_reason{namespace=\"$namespace\", reason=\"ContainerCannotRun\"} == 1) * 7",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "G"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_container_status_terminated_reason{namespace=\"$namespace\", reason=\"Error\"} == 1) * 8",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "H"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "(kube_pod_container_status_terminated_reason{namespace=\"$namespace\", reason=\"OOMKilled\"} == 1) * 9",
|
||||||
|
"format": "time_series",
|
||||||
|
"hide": false,
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "{{ pod }} - {{ container }}",
|
||||||
|
"refId": "I"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Container Status",
|
||||||
|
"tooltip": {
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
"transparent": true,
|
||||||
|
"type": "flant-statusmap-panel",
|
||||||
|
"useMax": true,
|
||||||
|
"xAxis": {
|
||||||
|
"labelFormat": "%a %m/%d",
|
||||||
|
"minBucketWidthToShowWeekends": 4,
|
||||||
|
"show": true,
|
||||||
|
"showCrosshair": true,
|
||||||
|
"showWeekends": true
|
||||||
|
},
|
||||||
|
"yAxis": {
|
||||||
|
"show": true,
|
||||||
|
"showCrosshair": false
|
||||||
|
},
|
||||||
|
"yAxisSort": "a → z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 16,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"allValue": null,
|
||||||
|
"current": {
|
||||||
|
"tags": [],
|
||||||
|
"text": "openshift-monitoring",
|
||||||
|
"value": "openshift-monitoring"
|
||||||
|
},
|
||||||
|
"datasource": "prometheus",
|
||||||
|
"definition": "label_values(kube_pod_info, namespace)",
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Namespace",
|
||||||
|
"multi": false,
|
||||||
|
"name": "namespace",
|
||||||
|
"options": [],
|
||||||
|
"query": "label_values(kube_pod_info, namespace)",
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"sort": 2,
|
||||||
|
"tagValuesQuery": "",
|
||||||
|
"tags": [],
|
||||||
|
"tagsQuery": "",
|
||||||
|
"type": "query",
|
||||||
|
"useTags": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-3h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Pod status",
|
||||||
|
"uid": "x3PAP_ymk",
|
||||||
|
"version": 10
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "status-hetamap",
|
"name": "flant-statusmap-panel",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"description": "Status Heatmap panel plugin for grafana",
|
"description": "Grafana panel plugin to visualize status of multiple objects over time",
|
||||||
"main": "README.md",
|
"main": "README.md",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "grunt"
|
"build": "grunt"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/flant/grafana-status-heatmap.git"
|
"url": "https://github.com/flant/grafana-statusmap.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel": "^6.23.0",
|
"babel": "^6.23.0",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"grunt-contrib-uglify": "^3.0.1",
|
"grunt-contrib-uglify": "^3.0.1",
|
||||||
"grunt-contrib-watch": "^1.0.0",
|
"grunt-contrib-watch": "^1.0.0",
|
||||||
"grunt-execute": "^0.2.2",
|
"grunt-execute": "^0.2.2",
|
||||||
|
"grunt-notify": "^0.4.5",
|
||||||
"grunt-systemjs-builder": "^1.0.0",
|
"grunt-systemjs-builder": "^1.0.0",
|
||||||
"load-grunt-tasks": "^3.5.2"
|
"load-grunt-tasks": "^3.5.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import d3 from 'd3';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
|
||||||
|
let TOOLTIP_PADDING_X = 30;
|
||||||
|
let TOOLTIP_PADDING_Y = 10;
|
||||||
|
|
||||||
|
export class AnnotationTooltip {
|
||||||
|
constructor(elem, scope) {
|
||||||
|
this.scope = scope;
|
||||||
|
this.dashboard = scope.ctrl.dashboard;
|
||||||
|
this.panelCtrl = scope.ctrl;
|
||||||
|
this.panel = scope.ctrl.panel;
|
||||||
|
this.mouseOverAnnotationTick = false;
|
||||||
|
|
||||||
|
elem.on("mouseover", this.onMouseOver.bind(this));
|
||||||
|
elem.on("mouseleave", this.onMouseLeave.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseOver(e) {
|
||||||
|
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) { return; }
|
||||||
|
|
||||||
|
if (!this.tooltip) {
|
||||||
|
this.add();
|
||||||
|
this.move(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseLeave() {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(e) {
|
||||||
|
if (!this.panel.tooltip.show) { return; }
|
||||||
|
|
||||||
|
this.move(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
add() {
|
||||||
|
this.tooltipBase = d3.select("body")
|
||||||
|
.append("div")
|
||||||
|
.attr("class", "statusmap-annotation-tooltip drop drop-popover drop-popover--annotation drop-element drop-enabled drop-target-attached-center drop-open drop-open-transitionend drop-after-open")
|
||||||
|
.style("position", "absolute")
|
||||||
|
this.tooltip = this.tooltipBase
|
||||||
|
.append("div")
|
||||||
|
.attr("class", "drop-content")
|
||||||
|
.append("div")
|
||||||
|
.append("annotation-tooltip")
|
||||||
|
.append("div")
|
||||||
|
.attr("class", "graph-annotation");
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.tooltip) {
|
||||||
|
this.tooltip.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltip = null;
|
||||||
|
|
||||||
|
if (this.tooltipBase) {
|
||||||
|
this.tooltipBase.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltipBase = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
show(pos) {
|
||||||
|
if (!this.panel.tooltip.show || !this.tooltip) { return; }
|
||||||
|
// shared tooltip mode
|
||||||
|
//if (pos.panelRelY) {
|
||||||
|
// return;
|
||||||
|
//}
|
||||||
|
|
||||||
|
let annoId = d3.select(pos.target).attr('annoId');
|
||||||
|
if (!annoId) {
|
||||||
|
this.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let anno = this.panelCtrl.annotations[annoId];
|
||||||
|
if (!anno) {
|
||||||
|
this.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let annoTitle = "";
|
||||||
|
|
||||||
|
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
let annoTime = this.dashboard.formatDate(anno.time, tooltipTimeFormat);
|
||||||
|
let annoText = anno.text;
|
||||||
|
let annoTags = [];
|
||||||
|
if (anno.tags) {
|
||||||
|
annoTags = _.map(anno.tags, t => ({"text": t, "backColor": "rgb(63, 43, 91)", "borderColor":"rgb(101, 81, 129)"}))
|
||||||
|
}
|
||||||
|
|
||||||
|
let tooltipHtml = `<div class="graph-annotation__header">
|
||||||
|
<span class="graph-annotation__title">${annoTitle}</span>
|
||||||
|
<span class="graph-annotation__time">${annoTime}</span></div>
|
||||||
|
<div class="graph-annotation__body">
|
||||||
|
<div>${annoText}</div>
|
||||||
|
${_.join(_.map(annoTags, t => `<span class="label label-tag small" style="background-color: ${t.backColor}; border-color: ${t.borderColor}">${t.text}</span>`), "")}
|
||||||
|
</div>
|
||||||
|
<div class="statusmap-histogram"></div>`;
|
||||||
|
|
||||||
|
this.tooltip.html(tooltipHtml);
|
||||||
|
|
||||||
|
this.move(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
move(pos) {
|
||||||
|
if (!this.tooltipBase) { return; }
|
||||||
|
|
||||||
|
let elem = $(this.tooltipBase.node())[0];
|
||||||
|
let tooltipWidth = elem.clientWidth;
|
||||||
|
let tooltipHeight = elem.clientHeight;
|
||||||
|
|
||||||
|
let left = pos.pageX - tooltipWidth/2;
|
||||||
|
let top = pos.pageY + TOOLTIP_PADDING_Y;
|
||||||
|
|
||||||
|
if (pos.pageX + tooltipWidth/2 + 10 > window.innerWidth) {
|
||||||
|
left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos.pageY - window.pageYOffset + tooltipHeight + 20 > window.innerHeight) {
|
||||||
|
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tooltipBase
|
||||||
|
.style("left", left + "px")
|
||||||
|
.style("top", top + "px");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ import {tickStep} from 'app/core/utils/ticks';
|
|||||||
|
|
||||||
let mod = angular.module('grafana.directives');
|
let mod = angular.module('grafana.directives');
|
||||||
|
|
||||||
const MIN_LEGEND_STEPS = 10;
|
const LEGEND_STEP_WIDTH = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Color legend for heatmap editor.
|
* Bigger color legend for opacity and spectrum modes editor.
|
||||||
*/
|
*/
|
||||||
mod.directive('optionsColorLegend', function() {
|
mod.directive('optionsColorLegend', function() {
|
||||||
return {
|
return {
|
||||||
@@ -45,7 +45,7 @@ mod.directive('optionsColorLegend', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Heatmap legend with scale values.
|
* Graph legend with values.
|
||||||
*/
|
*/
|
||||||
mod.directive('statusHeatmapLegend', function() {
|
mod.directive('statusHeatmapLegend', function() {
|
||||||
return {
|
return {
|
||||||
@@ -62,6 +62,9 @@ mod.directive('statusHeatmapLegend', function() {
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
if (!ctrl.panel.legend.show) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
||||||
let rangeFrom = ctrl.cardsData.minValue;
|
let rangeFrom = ctrl.cardsData.minValue;
|
||||||
let rangeTo = ctrl.cardsData.maxValue;
|
let rangeTo = ctrl.cardsData.maxValue;
|
||||||
@@ -92,15 +95,9 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
|
|||||||
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
||||||
let legendHeight = legendElem.attr("height");
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
let rangeStep = 1;
|
let rangeStep = (rangeTo - rangeFrom) / (legendWidth/LEGEND_STEP_WIDTH);
|
||||||
if (rangeTo - rangeFrom > legendWidth) {
|
// width in pixels in legend space of unit segment in range space
|
||||||
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
// rangeStep * witdhFactor == width in pixels of one rangeStep
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
|
||||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
@@ -108,9 +105,12 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
|
|||||||
legend.selectAll(".status-heatmap-color-legend-rect")
|
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||||
.data(valuesRange)
|
.data(valuesRange)
|
||||||
.enter().append("rect")
|
.enter().append("rect")
|
||||||
.attr("x", d => d * widthFactor + 10) // shift all color rectangles to the right
|
// translate from range space into pixels
|
||||||
|
// and shift all rectangles to the right by 10
|
||||||
|
.attr("x", d => d * widthFactor+10)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
|
// rectangles are slightly overlaped to prevent gaps
|
||||||
|
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||||
.attr("height", legendHeight)
|
.attr("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", d => colorScale(d));
|
.attr("fill", d => colorScale(d));
|
||||||
@@ -126,22 +126,22 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
|
|||||||
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
|
||||||
let legendHeight = legendElem.attr("height");
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
let rangeStep = 10;
|
let rangeStep = (rangeTo - rangeFrom) / (legendWidth/LEGEND_STEP_WIDTH);
|
||||||
|
// width in pixels in legend space of unit segment in range space
|
||||||
|
// rangeStep * witdhFactor == width in pixels of one rangeStep
|
||||||
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
let widthFactor = legendWidth / (rangeTo - rangeFrom);
|
||||||
|
|
||||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
|
||||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||||
|
|
||||||
let opacityScale = getOpacityScale(options, maxValue, minValue);
|
let opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||||
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
||||||
.data(valuesRange)
|
.data(valuesRange)
|
||||||
.enter().append("rect")
|
.enter().append("rect")
|
||||||
.attr("x", d => d * widthFactor + 10) // shift all opacity rectangles to the right
|
// translate from range space into pixels
|
||||||
|
// and shift all rectangles to the right by 10
|
||||||
|
.attr("x", d => d * widthFactor+10)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", rangeStep * widthFactor)
|
// rectangles are slightly overlaped to prevent gaps
|
||||||
|
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||||
.attr("height", legendHeight)
|
.attr("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", options.cardColor)
|
.attr("fill", options.cardColor)
|
||||||
@@ -168,14 +168,14 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteHelper) {
|
|||||||
legend.selectAll(".hidden-texts")
|
legend.selectAll(".hidden-texts")
|
||||||
.data(tooltips)
|
.data(tooltips)
|
||||||
.enter().append("text")
|
.enter().append("text")
|
||||||
.attr("class", "axis tick")
|
.attr("class", "axis tick hidden-texts")
|
||||||
.attr("font-family", "sans-serif")
|
.attr("font-family", "sans-serif")
|
||||||
.text(d => d)
|
.text(d => d)
|
||||||
.each(function(d,i) {
|
.each(function(d,i) {
|
||||||
let thisWidth = this.getBBox().width;
|
let thisWidth = this.getBBox().width;
|
||||||
textWidth.push(thisWidth);
|
textWidth.push(thisWidth);
|
||||||
this.remove(); // remove them just after displaying them
|
|
||||||
});
|
});
|
||||||
|
legend.selectAll(".hidden-texts").remove();
|
||||||
|
|
||||||
let legendWidth = Math.floor(_.min([
|
let legendWidth = Math.floor(_.min([
|
||||||
graphWidth - 30,
|
graphWidth - 30,
|
||||||
@@ -289,23 +289,21 @@ function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
|
|||||||
|
|
||||||
function drawSimpleColorLegend(elem, colorScale) {
|
function drawSimpleColorLegend(elem, colorScale) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
|
let legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
let legendHeight = legendElem.attr("height");
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
if (legendWidth) {
|
if (legendWidth) {
|
||||||
let valuesNumber = Math.floor(legendWidth / 2);
|
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||||
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
|
||||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
|
|
||||||
let legend = d3.select(legendElem.get(0));
|
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||||
var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange);
|
.data(valuesRange)
|
||||||
|
.enter().append("rect")
|
||||||
legendRects.enter().append("rect")
|
|
||||||
.attr("x", d => d)
|
.attr("x", d => d)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", rangeStep + 1) // Overlap rectangles to prevent gaps
|
.attr("width", LEGEND_STEP_WIDTH + 1) // Overlap rectangles to prevent gaps
|
||||||
.attr("height", legendHeight)
|
.attr("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", d => colorScale(d));
|
.attr("fill", d => colorScale(d));
|
||||||
@@ -314,10 +312,9 @@ function drawSimpleColorLegend(elem, colorScale) {
|
|||||||
|
|
||||||
function drawSimpleOpacityLegend(elem, options) {
|
function drawSimpleOpacityLegend(elem, options) {
|
||||||
let legendElem = $(elem).find('svg');
|
let legendElem = $(elem).find('svg');
|
||||||
let graphElem = $(elem);
|
let legend = d3.select(legendElem.get(0));
|
||||||
clearLegend(elem);
|
clearLegend(elem);
|
||||||
|
|
||||||
let legend = d3.select(legendElem.get(0));
|
|
||||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||||
let legendHeight = legendElem.attr("height");
|
let legendHeight = legendElem.attr("height");
|
||||||
|
|
||||||
@@ -333,14 +330,14 @@ function drawSimpleOpacityLegend(elem, options) {
|
|||||||
.range([0, 1]);
|
.range([0, 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rangeStep = 10;
|
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
|
||||||
var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange);
|
|
||||||
|
|
||||||
legendRects.enter().append("rect")
|
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
||||||
|
.data(valuesRange)
|
||||||
|
.enter().append("rect")
|
||||||
.attr("x", d => d)
|
.attr("x", d => d)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", rangeStep)
|
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||||
.attr("height", legendHeight)
|
.attr("height", legendHeight)
|
||||||
.attr("stroke-width", 0)
|
.attr("stroke-width", 0)
|
||||||
.attr("fill", options.cardColor)
|
.attr("fill", options.cardColor)
|
||||||
|
|||||||
@@ -61,6 +61,30 @@ export class ColorModeDiscrete {
|
|||||||
getBucketColor(values) {
|
getBucketColor(values) {
|
||||||
let thresholds = this.panel.color.thresholds;
|
let thresholds = this.panel.color.thresholds;
|
||||||
|
|
||||||
|
if (!values || values.length == 0) {
|
||||||
|
// treat as null value
|
||||||
|
return this.getMatchedThreshold(null).color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.length == 1) {
|
||||||
|
let threshold = this.getMatchedThreshold(values[0]);
|
||||||
|
if (!threshold || !threshold.color || threshold.color == "") {
|
||||||
|
return 'rgba(0,0,0,1)';
|
||||||
|
} else {
|
||||||
|
return threshold.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isAllValuesNulls = true;
|
||||||
|
for (let j = 0; j < values.length; j++) {
|
||||||
|
if (values[j] != null) {
|
||||||
|
isAllValuesNulls = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isAllValuesNulls) {
|
||||||
|
return this.getMatchedThreshold(null).color;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < thresholds.length; i++) {
|
for (let i = 0; i < thresholds.length; i++) {
|
||||||
for (let j = 0; j < values.length; j++) {
|
for (let j = 0; j < values.length; j++) {
|
||||||
if (values[j] == thresholds[i].value) {
|
if (values[j] == thresholds[i].value) {
|
||||||
@@ -93,7 +117,7 @@ export class ColorModeDiscrete {
|
|||||||
|
|
||||||
getMatchedThreshold(value) {
|
getMatchedThreshold(value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
if (this.panel.color.nullPointMode == 'as empty') {
|
if (this.panel.nullPointMode == 'as empty') {
|
||||||
// FIXME: make this explicit for user
|
// FIXME: make this explicit for user
|
||||||
// Right now this color never used because null as empty handles in getCardOpacity method.
|
// Right now this color never used because null as empty handles in getCardOpacity method.
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
// position: relative;
|
// position: relative;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
|
|
||||||
|
// allow tooltips on hover of warning text
|
||||||
|
.datapoints-warning {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.datapoints-warning span {
|
.datapoints-warning span {
|
||||||
background-color: $warning-bg;
|
background-color: $warning-bg;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -1,7 +1,7 @@
|
|||||||
<div class="status-heatmap-wrapper">
|
<div class="status-heatmap-wrapper">
|
||||||
<div class="status-heatmap-canvas-wrapper">
|
<div class="status-heatmap-canvas-wrapper">
|
||||||
|
|
||||||
<div class="datapoints-warning">
|
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined">
|
||||||
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
|
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
|
||||||
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
|
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,40 +55,70 @@
|
|||||||
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
<div class="gf-form" ng-show="ctrl.panel.color.thresholds.length == 0">
|
||||||
|
<label class="gf-form-label width-2">0</label>
|
||||||
<label class="gf-form-label width-2">{{ $index }}</label>
|
<label class="gf-form-input width-30">No statuses defined. Add new status or use presets below.</label>
|
||||||
<label class="gf-form-label width-9" ng-if="ctrl.panel.useMax">If bucket has value</label>
|
|
||||||
<label class="gf-form-label width-9" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
|
|
||||||
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
|
|
||||||
<label class="gf-form-label">
|
|
||||||
fill
|
|
||||||
</label>
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
|
|
||||||
</label>
|
|
||||||
<label class="gf-form-label">set tooltip</label>
|
|
||||||
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
|
|
||||||
|
|
||||||
<label class="gf-form-label">
|
|
||||||
<a class="pointer" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
|
||||||
<i class="fa fa-trash"/>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
||||||
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
|
<div class="gf-form">
|
||||||
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
|
<label class="gf-form-label width-2">{{ $index }}</label>
|
||||||
Add trafic lights
|
</div>
|
||||||
</a>
|
<div class="gf-form">
|
||||||
</label>
|
<label class="gf-form-label width-9 query-keyword" ng-if="ctrl.panel.useMax">If bucket has value</label>
|
||||||
<label class="gf-form-label">
|
<label class="gf-form-label width-9 query-keyword" ng-if="!ctrl.panel.useMax">If bucket value ==</label>
|
||||||
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
|
<input type="text" class="input-small gf-form-input width-4" ng-model="threshold.value" ng-change="ctrl.render()"/>
|
||||||
Add new status
|
<label class="gf-form-label query-keyword">
|
||||||
</a>
|
fill
|
||||||
</label>
|
</label>
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<spectrum-picker ng-model="threshold.color" ng-change="ctrl.render()"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label query-keyword">set tooltip</label>
|
||||||
|
<input type="text" class="input-small gf-form-input width-9" ng-model="threshold.tooltip" ng-change="ctrl.render()"/>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">
|
||||||
|
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
||||||
|
<i class="fa fa-trash"/>
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-inline">
|
||||||
|
<div class="gf-form"></div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn btn-inverse" ng-click="ctrl.onEditorAddThreshold()">
|
||||||
|
<i class="fa fa-plus"></i> Add new status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn btn-inverse" ng-click="ctrl.onEditorRemoveThresholds()">
|
||||||
|
<i class="fa fa-minus"></i> Remove all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-inline" ng-show="ctrl.panel.color.thresholds.length == 0">
|
||||||
|
<div class="gf-form"></div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label">Presets</label>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn" ng-click="ctrl.onEditorAddThreeLights()">
|
||||||
|
Red-Yellow-Green
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn" ng-click="ctrl.onEditorAddSolarized()">
|
||||||
|
Solarized
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
<div class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
||||||
@@ -118,7 +148,16 @@
|
|||||||
label="Show tooltip"
|
label="Show tooltip"
|
||||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
</div>
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-8">Y axis sort</label>
|
||||||
|
<div class="gf-form-select-wrapper">
|
||||||
|
<select class="gf-form-input max-width-8"
|
||||||
|
ng-model="ctrl.panel.yAxisSort"
|
||||||
|
ng-options="f for f in ['metrics', 'a → z', 'z → a']"
|
||||||
|
ng-change="ctrl.render()"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section gf-form-group">
|
<div class="section gf-form-group">
|
||||||
<h5 class="section-heading">Bucket</h5>
|
<h5 class="section-heading">Bucket</h5>
|
||||||
@@ -142,12 +181,16 @@
|
|||||||
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Spacing</label>
|
<label class="gf-form-label width-9">V spacing</label>
|
||||||
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Card spacing in pixels'" ng-model="ctrl.panel.cards.cardSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards vertical spacing in pixels'" ng-model="ctrl.panel.cards.cardVSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-9">H spacing</label>
|
||||||
|
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards horizontal spacing in pixels'" ng-model="ctrl.panel.cards.cardHSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-9">Rounding</label>
|
<label class="gf-form-label width-9">Rounding</label>
|
||||||
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Angles rounding in percent'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
|
<input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Cards rounding radius in pixels'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
{"name": "Color mapping", "path": "img/color-mapping.png"},
|
{"name": "Color mapping", "path": "img/color-mapping.png"},
|
||||||
{"name": "Queries example", "path": "img/queries-example.png"}
|
{"name": "Queries example", "path": "img/queries-example.png"}
|
||||||
],
|
],
|
||||||
"version": "0.0.2",
|
"version": "0.1.1",
|
||||||
"updated": "2018-10-17"
|
"updated": "2019-03-22"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks
|
|||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
|
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
|
||||||
import {StatusHeatmapTooltip} from './tooltip';
|
import {StatusHeatmapTooltip} from './tooltip';
|
||||||
|
import {AnnotationTooltip} from './annotations';
|
||||||
|
|
||||||
let MIN_CARD_SIZE = 5,
|
let MIN_CARD_SIZE = 5,
|
||||||
CARD_SPACING = 2,
|
CARD_H_SPACING = 2,
|
||||||
|
CARD_V_SPACING = 2,
|
||||||
CARD_ROUND = 0,
|
CARD_ROUND = 0,
|
||||||
DATA_RANGE_WIDING_FACTOR = 1.2,
|
DATA_RANGE_WIDING_FACTOR = 1.2,
|
||||||
DEFAULT_X_TICK_SIZE_PX = 100,
|
DEFAULT_X_TICK_SIZE_PX = 100,
|
||||||
@@ -24,13 +26,14 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
// $heatmap is JQuery object, but heatmap is D3
|
// $heatmap is JQuery object, but heatmap is D3
|
||||||
let $heatmap = elem.find('.status-heatmap-panel');
|
let $heatmap = elem.find('.status-heatmap-panel');
|
||||||
let tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
let tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
||||||
|
let annotationTooltip = new AnnotationTooltip($heatmap, scope);
|
||||||
|
|
||||||
let width, height,
|
let width, height,
|
||||||
yScale, xScale,
|
yScale, xScale,
|
||||||
chartWidth, chartHeight,
|
chartWidth, chartHeight,
|
||||||
chartTop, chartBottom,
|
chartTop, chartBottom,
|
||||||
yAxisWidth, xAxisHeight,
|
yAxisWidth, xAxisHeight,
|
||||||
cardSpacing, cardRound,
|
cardVSpacing, cardHSpacing, cardRound,
|
||||||
cardWidth, cardHeight,
|
cardWidth, cardHeight,
|
||||||
colorScale, opacityScale,
|
colorScale, opacityScale,
|
||||||
mouseUpHandler,
|
mouseUpHandler,
|
||||||
@@ -76,7 +79,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
return text.getBBox().width;
|
return text.getBBox().width;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return max_text_width;
|
return Math.ceil(max_text_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getXAxisHeight(elem) {
|
function getXAxisHeight(elem) {
|
||||||
@@ -129,9 +132,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
function getYScale(ticks) {
|
function getYScale(ticks) {
|
||||||
let range = [];
|
let range = [];
|
||||||
let step = chartHeight / ticks.length;
|
let step = chartHeight / ticks.length;
|
||||||
range.push(chartHeight);
|
// svg has y=0 on the top, so top card should have a minimal value in range
|
||||||
|
range.push(step);
|
||||||
for (let i = 1; i < ticks.length; i++) {
|
for (let i = 1; i < ticks.length; i++) {
|
||||||
range.push(chartHeight - step * i);
|
range.push(step * (i+1));
|
||||||
}
|
}
|
||||||
return d3.scaleOrdinal()
|
return d3.scaleOrdinal()
|
||||||
.domain(ticks)
|
.domain(ticks)
|
||||||
@@ -142,9 +146,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
function getYAxisScale(ticks) {
|
function getYAxisScale(ticks) {
|
||||||
let range = [];
|
let range = [];
|
||||||
let step = chartHeight / ticks.length;
|
let step = chartHeight / ticks.length;
|
||||||
range.push(chartHeight - yOffset);
|
// svg has y=0 on the top, so top tick should have a minimal value in range
|
||||||
|
range.push(yOffset);
|
||||||
for (let i = 1; i < ticks.length; i++) {
|
for (let i = 1; i < ticks.length; i++) {
|
||||||
range.push(chartHeight - step * i - yOffset);
|
range.push(step * i + yOffset);
|
||||||
}
|
}
|
||||||
return d3.scaleOrdinal()
|
return d3.scaleOrdinal()
|
||||||
.domain(ticks)
|
.domain(ticks)
|
||||||
@@ -159,6 +164,12 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
ticks = [''];
|
ticks = [''];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (panel.yAxisSort == 'a → z') {
|
||||||
|
ticks.sort((a, b) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
|
||||||
|
} else if (panel.yAxisSort == 'z → a') {
|
||||||
|
ticks.sort((b, a) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
|
||||||
|
}
|
||||||
|
|
||||||
let yAxisScale = getYAxisScale(ticks);
|
let yAxisScale = getYAxisScale(ticks);
|
||||||
scope.yScale = yScale = getYScale(ticks);
|
scope.yScale = yScale = getYScale(ticks);
|
||||||
|
|
||||||
@@ -231,12 +242,13 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
chartTop = margin.top;
|
chartTop = margin.top;
|
||||||
chartBottom = chartTop + chartHeight;
|
chartBottom = chartTop + chartHeight;
|
||||||
|
|
||||||
cardSpacing = panel.cards.cardSpacing !== null ? panel.cards.cardSpacing : CARD_SPACING;
|
cardHSpacing = panel.cards.cardHSpacing !== null ? panel.cards.cardHSpacing : CARD_H_SPACING;
|
||||||
|
cardVSpacing = panel.cards.cardVSpacing !== null ? panel.cards.cardVSpacing : CARD_V_SPACING;
|
||||||
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
|
cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
|
||||||
|
|
||||||
// calculate yOffset for YAxis
|
// calculate yOffset for YAxis
|
||||||
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
||||||
cardHeight = yGridSize ? yGridSize - cardSpacing : 0;
|
cardHeight = yGridSize ? yGridSize - cardVSpacing : 0;
|
||||||
yOffset = cardHeight / 2;
|
yOffset = cardHeight / 2;
|
||||||
|
|
||||||
addYAxis();
|
addYAxis();
|
||||||
@@ -244,9 +256,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||||
chartWidth = width - yAxisWidth - margin.right;
|
chartWidth = width - yAxisWidth - margin.right;
|
||||||
|
|
||||||
|
// TODO allow per-y cardWidth!
|
||||||
// we need to fill chartWidth with xBucketSize cards.
|
// we need to fill chartWidth with xBucketSize cards.
|
||||||
xGridSize = chartWidth / (cardsData.xBucketSize+1);
|
xGridSize = chartWidth / (cardsData.xBucketSize+1);
|
||||||
cardWidth = xGridSize - cardSpacing;
|
cardWidth = xGridSize - cardHSpacing;
|
||||||
|
|
||||||
addXAxis();
|
addXAxis();
|
||||||
xAxisHeight = getXAxisHeight(heatmap);
|
xAxisHeight = getXAxisHeight(heatmap);
|
||||||
@@ -299,6 +312,8 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
resetCardHighLight(event);
|
resetCardHighLight(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_renderAnnotations();
|
||||||
|
|
||||||
ctrl.events.emit('render-complete', {
|
ctrl.events.emit('render-complete', {
|
||||||
"chartWidth": chartWidth
|
"chartWidth": chartWidth
|
||||||
});
|
});
|
||||||
@@ -356,7 +371,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
let cx = xScale(d.x);
|
let cx = xScale(d.x);
|
||||||
|
|
||||||
if (cx - cardWidth/2 < 0) {
|
if (cx - cardWidth/2 < 0) {
|
||||||
x = yAxisWidth + cardSpacing/2;
|
x = yAxisWidth + cardHSpacing/2;
|
||||||
} else {
|
} else {
|
||||||
x = yAxisWidth + cx - cardWidth/2;
|
x = yAxisWidth + cx - cardWidth/2;
|
||||||
}
|
}
|
||||||
@@ -372,11 +387,11 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
if (cx < cardWidth/2) {
|
if (cx < cardWidth/2) {
|
||||||
// Center should not exceed half of card.
|
// Center should not exceed half of card.
|
||||||
// Cut card to the left to prevent overlay of y axis.
|
// Cut card to the left to prevent overlay of y axis.
|
||||||
let cutted_width = (cx - cardSpacing/2) + cardWidth/2;
|
let cutted_width = (cx - cardHSpacing/2) + cardWidth/2;
|
||||||
w = cutted_width > 0 ? cutted_width : 0;
|
w = cutted_width > 0 ? cutted_width : 0;
|
||||||
} else if (chartWidth - cx < cardWidth/2) {
|
} else if (chartWidth - cx < cardWidth/2) {
|
||||||
// Cut card to the right to prevent overlay of right graph edge.
|
// Cut card to the right to prevent overlay of right graph edge.
|
||||||
w = cardWidth/2 + (chartWidth - cx - cardSpacing/2);
|
w = cardWidth/2 + (chartWidth - cx - cardHSpacing/2);
|
||||||
} else {
|
} else {
|
||||||
w = cardWidth;
|
w = cardWidth;
|
||||||
}
|
}
|
||||||
@@ -384,21 +399,25 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
// Card width should be MIN_CARD_SIZE at least
|
// Card width should be MIN_CARD_SIZE at least
|
||||||
w = Math.max(w, MIN_CARD_SIZE);
|
w = Math.max(w, MIN_CARD_SIZE);
|
||||||
|
|
||||||
|
if (cardHSpacing == 0) {
|
||||||
|
w = w+1;
|
||||||
|
}
|
||||||
|
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardY(d) {
|
function getCardY(d) {
|
||||||
return yScale(d.y) + chartTop - cardHeight - cardSpacing/2;
|
return yScale(d.y) + chartTop - cardHeight - cardVSpacing/2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardHeight(d) {
|
function getCardHeight(d) {
|
||||||
let ys = yScale(d.y);
|
let ys = yScale(d.y);
|
||||||
let y = ys + chartTop - cardHeight - cardSpacing/2;
|
let y = ys + chartTop - cardHeight - cardVSpacing/2;
|
||||||
let h = cardHeight;
|
let h = cardHeight;
|
||||||
|
|
||||||
// Cut card height to prevent overlay
|
// Cut card height to prevent overlay
|
||||||
if (y < chartTop) {
|
if (y < chartTop) {
|
||||||
h = ys - cardSpacing/2;
|
h = ys - cardVSpacing/2;
|
||||||
} else if (ys > chartBottom) {
|
} else if (ys > chartBottom) {
|
||||||
h = chartBottom - y;
|
h = chartBottom - y;
|
||||||
} else if (y + cardHeight > chartBottom) {
|
} else if (y + cardHeight > chartBottom) {
|
||||||
@@ -410,6 +429,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
// Card height should be MIN_CARD_SIZE at least
|
// Card height should be MIN_CARD_SIZE at least
|
||||||
h = Math.max(h, MIN_CARD_SIZE);
|
h = Math.max(h, MIN_CARD_SIZE);
|
||||||
|
|
||||||
|
if (cardVSpacing == 0) {
|
||||||
|
h = h+1
|
||||||
|
}
|
||||||
|
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +510,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
function onMouseLeave() {
|
function onMouseLeave() {
|
||||||
appEvents.emit('graph-hover-clear');
|
appEvents.emit('graph-hover-clear');
|
||||||
clearCrosshair();
|
clearCrosshair();
|
||||||
|
//annotationTooltip.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove(event) {
|
function onMouseMove(event) {
|
||||||
@@ -496,6 +520,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
// Clear crosshair and tooltip
|
// Clear crosshair and tooltip
|
||||||
clearCrosshair();
|
clearCrosshair();
|
||||||
tooltip.destroy();
|
tooltip.destroy();
|
||||||
|
annotationTooltip.destroy();
|
||||||
|
|
||||||
selection.x2 = limitSelection(event.offsetX);
|
selection.x2 = limitSelection(event.offsetX);
|
||||||
drawSelection(selection.x1, selection.x2);
|
drawSelection(selection.x1, selection.x2);
|
||||||
@@ -503,6 +528,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
emitGraphHoverEvet(event);
|
emitGraphHoverEvet(event);
|
||||||
drawCrosshair(event.offsetX);
|
drawCrosshair(event.offsetX);
|
||||||
tooltip.show(event); //, data);
|
tooltip.show(event); //, data);
|
||||||
|
annotationTooltip.show(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,6 +644,62 @@ export default function link(scope, elem, attrs, ctrl) {
|
|||||||
$heatmap.on("mousedown", onMouseDown);
|
$heatmap.on("mousedown", onMouseDown);
|
||||||
$heatmap.on("mousemove", onMouseMove);
|
$heatmap.on("mousemove", onMouseMove);
|
||||||
$heatmap.on("mouseleave", onMouseLeave);
|
$heatmap.on("mouseleave", onMouseLeave);
|
||||||
|
|
||||||
|
function _renderAnnotations() {
|
||||||
|
if (!ctrl.annotations || ctrl.annotations.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!heatmap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let annoData = _.map(ctrl.annotations, (d,i) => ({"x": Math.floor(yAxisWidth + xScale(d.time)), "id":i, "anno": d.source}));
|
||||||
|
|
||||||
|
|
||||||
|
let anno = heatmap
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "statusmap-annotations")
|
||||||
|
.attr("transform", "translate(0.5,0)")
|
||||||
|
.selectAll(".statusmap-annotations")
|
||||||
|
.data(annoData)
|
||||||
|
.enter().append("g")
|
||||||
|
;
|
||||||
|
anno.append("line")
|
||||||
|
//.attr("class", "statusmap-annotation-tick")
|
||||||
|
.attr("x1", d => d.x)
|
||||||
|
.attr("y1", chartTop)
|
||||||
|
.attr("x2", d => d.x)
|
||||||
|
.attr("y2", chartBottom)
|
||||||
|
.style("stroke", d => d.anno.iconColor)
|
||||||
|
.style("stroke-width", 1)
|
||||||
|
.style("stroke-dasharray", "3,3")
|
||||||
|
;
|
||||||
|
anno.append("polygon")
|
||||||
|
.attr("points", d => [[d.x, chartBottom+1], [d.x-5, chartBottom+6], [d.x+5, chartBottom+6]].join(" "))
|
||||||
|
.style("stroke-width", 0)
|
||||||
|
.style("fill", d => d.anno.iconColor)
|
||||||
|
;
|
||||||
|
// Polygons didn't fire mouseevents
|
||||||
|
anno.append("rect")
|
||||||
|
.attr("x", d => d.x-5)
|
||||||
|
.attr("width", 10)
|
||||||
|
.attr("y", chartBottom+1)
|
||||||
|
.attr("height", 5)
|
||||||
|
.attr("class", "statusmap-annotation-tick")
|
||||||
|
.attr("annoId", d => d.id)
|
||||||
|
.style("opacity", 0)
|
||||||
|
;
|
||||||
|
|
||||||
|
let $ticks = $heatmap.find(".statusmap-annotation-tick");
|
||||||
|
$ticks.on("mouseenter", (event) => {
|
||||||
|
annotationTooltip.mouseOverAnnotationTick = true;
|
||||||
|
})
|
||||||
|
.on("mouseleave", (event) => {
|
||||||
|
annotationTooltip.mouseOverAnnotationTick = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function grafanaTimeFormat(ticks, min, max) {
|
function grafanaTimeFormat(ticks, min, max) {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ const panelDefaults = {
|
|||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
cardMinWidth: 5,
|
cardMinWidth: 5,
|
||||||
cardSpacing: 2,
|
cardVSpacing: 2,
|
||||||
|
cardHSpacing: 2,
|
||||||
cardRound: null
|
cardRound: null
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@@ -57,6 +58,7 @@ const panelDefaults = {
|
|||||||
},
|
},
|
||||||
// how null points should be handled
|
// how null points should be handled
|
||||||
nullPointMode: 'as empty',
|
nullPointMode: 'as empty',
|
||||||
|
yAxisSort: 'metrics',
|
||||||
highlightCards: true,
|
highlightCards: true,
|
||||||
useMax: true
|
useMax: true
|
||||||
};
|
};
|
||||||
@@ -98,7 +100,8 @@ let opacityScales = ['linear', 'sqrt'];
|
|||||||
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||||
static templateUrl = 'module.html';
|
static templateUrl = 'module.html';
|
||||||
|
|
||||||
constructor($scope, $injector, $rootScope, timeSrv) {
|
/** @ngInject */
|
||||||
|
constructor($scope, $injector, $rootScope, timeSrv, annotationsSrv) {
|
||||||
super($scope, $injector);
|
super($scope, $injector);
|
||||||
_.defaultsDeep(this.panel, panelDefaults);
|
_.defaultsDeep(this.panel, panelDefaults);
|
||||||
|
|
||||||
@@ -127,6 +130,9 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.annotations = [];
|
||||||
|
this.annotationsSrv = annotationsSrv;
|
||||||
|
|
||||||
this.events.on('data-received', this.onDataReceived);
|
this.events.on('data-received', this.onDataReceived);
|
||||||
this.events.on('data-snapshot-load', this.onDataReceived);
|
this.events.on('data-snapshot-load', this.onDataReceived);
|
||||||
this.events.on('data-error', this.onDataError);
|
this.events.on('data-error', this.onDataError);
|
||||||
@@ -152,7 +158,7 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
let minCardWidth = this.panel.cards.cardMinWidth;
|
let minCardWidth = this.panel.cards.cardMinWidth;
|
||||||
let minSpacing = this.panel.cards.cardSpacing;
|
let minSpacing = this.panel.cards.cardHSpacing;
|
||||||
let maxCardsCount = Math.ceil((chartWidth-minCardWidth) / (minCardWidth + minSpacing));
|
let maxCardsCount = Math.ceil((chartWidth-minCardWidth) / (minCardWidth + minSpacing));
|
||||||
|
|
||||||
let intervalMs;
|
let intervalMs;
|
||||||
@@ -187,11 +193,53 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
this.interval = kbn.secondsToHms(intervalMs / 1000);
|
this.interval = kbn.secondsToHms(intervalMs / 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
issueQueries = (datasource) => {
|
||||||
|
this.annotationsPromise = this.annotationsSrv.getAnnotations({
|
||||||
|
dashboard: this.dashboard,
|
||||||
|
panel: this.panel,
|
||||||
|
range: this.range,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Wait for annotationSrv requests to get datasources to
|
||||||
|
* resolve before issuing queries. This allows the annotations
|
||||||
|
* service to fire annotations queries before graph queries
|
||||||
|
* (but not wait for completion). This resolves
|
||||||
|
* issue 11806.
|
||||||
|
*/
|
||||||
|
// 5.x before 5.4 doesn't have dataPromises
|
||||||
|
if ("undefined" !== typeof(this.annotationsSrv.datasourcePromises)) {
|
||||||
|
return this.annotationsSrv.datasourcePromises.then(r => {
|
||||||
|
return super.issueQueries(datasource);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return super.issueQueries(datasource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onDataReceived = (dataList) => {
|
onDataReceived = (dataList) => {
|
||||||
this.data = dataList;
|
this.data = dataList;
|
||||||
this.cardsData = this.convertToCards(this.data);
|
this.cardsData = this.convertToCards(this.data);
|
||||||
|
|
||||||
this.render();
|
this.annotationsPromise.then(
|
||||||
|
result => {
|
||||||
|
this.loading = false;
|
||||||
|
//this.alertState = result.alertState;
|
||||||
|
if (result.annotations && result.annotations.length > 0) {
|
||||||
|
this.annotations = result.annotations;
|
||||||
|
} else {
|
||||||
|
this.annotations = null;
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.annotations = null;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
//this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
onInitEditMode = () => {
|
onInitEditMode = () => {
|
||||||
@@ -225,6 +273,7 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
|
|
||||||
onDataError = () => {
|
onDataError = () => {
|
||||||
this.data = [];
|
this.data = [];
|
||||||
|
this.annotations = [];
|
||||||
this.render();
|
this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -242,6 +291,11 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
this.render();
|
this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onEditorRemoveThresholds = () => {
|
||||||
|
this.panel.color.thresholds = [];
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
onEditorAddThreeLights = () => {
|
onEditorAddThreeLights = () => {
|
||||||
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
|
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
|
||||||
this.panel.color.thresholds.push({color: "yellow", value: 1, tooltip: "warning" });
|
this.panel.color.thresholds.push({color: "yellow", value: 1, tooltip: "warning" });
|
||||||
@@ -249,6 +303,19 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
this.render();
|
this.render();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* https://ethanschoonover.com/solarized/ */
|
||||||
|
onEditorAddSolarized = () => {
|
||||||
|
this.panel.color.thresholds.push({color: "#b58900", value: 0, tooltip: "yellow" });
|
||||||
|
this.panel.color.thresholds.push({color: "#cb4b16", value: 1, tooltip: "orange" });
|
||||||
|
this.panel.color.thresholds.push({color: "#dc322f", value: 2, tooltip: "red" });
|
||||||
|
this.panel.color.thresholds.push({color: "#d33682", value: 3, tooltip: "magenta" });
|
||||||
|
this.panel.color.thresholds.push({color: "#6c71c4", value: 4, tooltip: "violet" });
|
||||||
|
this.panel.color.thresholds.push({color: "#268bd2", value: 5, tooltip: "blue" });
|
||||||
|
this.panel.color.thresholds.push({color: "#2aa198", value: 6, tooltip: "cyan" });
|
||||||
|
this.panel.color.thresholds.push({color: "#859900", value: 7, tooltip: "green" });
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
link = (scope, elem, attrs, ctrl) => {
|
link = (scope, elem, attrs, ctrl) => {
|
||||||
rendering(scope, elem, attrs, ctrl);
|
rendering(scope, elem, attrs, ctrl);
|
||||||
};
|
};
|
||||||
@@ -263,28 +330,27 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
minValue: 0,
|
minValue: 0,
|
||||||
multipleValues: false,
|
multipleValues: false,
|
||||||
noColorDefined: false,
|
noColorDefined: false,
|
||||||
|
targets: [], // array of available unique targets
|
||||||
|
targetIndex: {} // indices in data array for each of available unique targets
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data || data.length == 0) { return cardsData;}
|
if (!data || data.length == 0) { return cardsData;}
|
||||||
|
|
||||||
// collect uniq targets and their indexes in data array
|
// Collect uniq timestamps from data and spread over targets and timestamps
|
||||||
cardsData.targetIndex = {};
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
// collect uniq targets and their indices
|
||||||
let ts = data[i];
|
_.map(data, (d, i) => {
|
||||||
let target = ts.target;
|
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i)
|
||||||
if (cardsData.targetIndex[target] == undefined) {
|
});
|
||||||
cardsData.targetIndex[target] = []
|
|
||||||
}
|
|
||||||
cardsData.targetIndex[target].push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO add some logic for targets heirarchy
|
// TODO add some logic for targets heirarchy
|
||||||
cardsData.targets = _.keys(cardsData.targetIndex);
|
cardsData.targets = _.keys(cardsData.targetIndex);
|
||||||
cardsData.targets.sort();
|
|
||||||
cardsData.yBucketSize = cardsData.targets.length;
|
cardsData.yBucketSize = cardsData.targets.length;
|
||||||
cardsData.xBucketSize = _.min(_.map(data, d => d.datapoints.length));
|
// Maximum number of buckets over x axis
|
||||||
|
cardsData.xBucketSize = _.max(_.map(data, d => d.datapoints.length));
|
||||||
|
|
||||||
// Collect all values for each bucket from datapoints with similar target.
|
// Collect all values for each bucket from datapoints with similar target.
|
||||||
|
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
|
||||||
for(let i = 0; i < cardsData.targets.length; i++) {
|
for(let i = 0; i < cardsData.targets.length; i++) {
|
||||||
let target = cardsData.targets[i];
|
let target = cardsData.targets[i];
|
||||||
|
|
||||||
@@ -294,15 +360,19 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
values: [],
|
values: [],
|
||||||
multipleValues: false,
|
multipleValues: false,
|
||||||
noColorDefined: false,
|
noColorDefined: false,
|
||||||
|
y: target,
|
||||||
|
x: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// collect values from all timeseries with target
|
// collect values from all timeseries with target
|
||||||
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
|
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
|
||||||
let s = data[cardsData.targetIndex[target][si]];
|
let s = data[cardsData.targetIndex[target][si]];
|
||||||
|
if (s.datapoints.length <= j) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let datapoint = s.datapoints[j];
|
let datapoint = s.datapoints[j];
|
||||||
if (card.values.length === 0) {
|
if (card.values.length === 0) {
|
||||||
card.x = datapoint[TIME_INDEX];
|
card.x = datapoint[TIME_INDEX];
|
||||||
card.y = s.target;
|
|
||||||
}
|
}
|
||||||
card.values.push(datapoint[VALUE_INDEX]);
|
card.values.push(datapoint[VALUE_INDEX]);
|
||||||
}
|
}
|
||||||
@@ -322,7 +392,9 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
|||||||
if (cardsData.minValue > card.minValue)
|
if (cardsData.minValue > card.minValue)
|
||||||
cardsData.minValue = card.minValue;
|
cardsData.minValue = card.minValue;
|
||||||
|
|
||||||
|
if (card.x != -1) {
|
||||||
cardsData.cards.push(card);
|
cardsData.cards.push(card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||