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 | ||
|
|
bccc3320a8 | ||
|
|
5d7cf54348 | ||
|
|
1f43ffe158 | ||
|
|
c7882402ce | ||
|
|
09cdd2f224 | ||
|
|
0a8463e2ed | ||
|
|
4d952a841b | ||
|
|
36e907f8b6 | ||
|
|
af55883905 | ||
|
|
afec01cb93 |
@@ -1,2 +1,4 @@
|
||||
node_modules
|
||||
.jshintrc
|
||||
.idea
|
||||
.sass-cache
|
||||
|
||||
@@ -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-contrib-clean');
|
||||
grunt.loadNpmTasks('grunt-notify');
|
||||
|
||||
grunt.initConfig({
|
||||
notify: {
|
||||
watch: {
|
||||
options: {
|
||||
message: 'grunt watch Complete',
|
||||
title: 'flant-statusmap-panel rebuilded',
|
||||
duration: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
clean: ['dist'],
|
||||
|
||||
@@ -17,7 +27,7 @@ module.exports = (grunt) => {
|
||||
},
|
||||
pluginDef: {
|
||||
expand: true,
|
||||
src: ['plugin.json', 'README.md'],
|
||||
src: ['plugin.json', 'README.md', 'CHANGELOG.md'],
|
||||
dest: 'dist'
|
||||
}
|
||||
},
|
||||
@@ -25,7 +35,7 @@ module.exports = (grunt) => {
|
||||
watch: {
|
||||
rebuild_all: {
|
||||
files: ['src/**/*', 'plugin.json'],
|
||||
tasks: ['default'],
|
||||
tasks: ['default', 'notify:watch'],
|
||||
options: {
|
||||
spawn: false,
|
||||
livereload: true
|
||||
@@ -53,7 +63,8 @@ module.exports = (grunt) => {
|
||||
sass: {
|
||||
dist: {
|
||||
files: {
|
||||
'dist/css/status-heatmap.css': 'src/css/status-heatmap.scss'
|
||||
'dist/css/statusmap.dark.css': 'src/css/statusmap.dark.scss',
|
||||
'dist/css/statusmap.light.css': 'src/css/statusmap.light.scss'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,33 @@
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Grouping values into rows and buckets using legend from query
|
||||
* User defined color mapping
|
||||
* Multiple values in bucket can be displayed via tooltip
|
||||
* Multiple values in bucket are displayed via tooltip
|
||||
* Increasing rows/buckets' interval for better visual representation
|
||||
* 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
|
||||
|
||||
* Prometheus datasource
|
||||
* Tested with Grafana 5.1.3
|
||||
* Tested with datasources:
|
||||
- Prometheus
|
||||
- InfluxDB
|
||||
- Mysql
|
||||
* Tested with Grafana:
|
||||
- 5.1.3
|
||||
- 5.4.3
|
||||
- 6.0.0
|
||||
|
||||
## Installation
|
||||
|
||||
Plugin can be installed via Git:
|
||||
Plugin can be installed with GF_INSTALL_PLUGINS="flant-statusmap-panel" or you can use Git and clone this repo:
|
||||
|
||||
```
|
||||
git clone git@github.com:flant/grafana-statusmap.git /var/lib/grafana/plugins/flant-statusmap-panel
|
||||
@@ -26,15 +37,19 @@ git clone git@github.com:flant/grafana-statusmap.git /var/lib/grafana/plugins/fl
|
||||
Alternatively, you can download [ZIP archive](https://github.com/flant/grafana-statusmap/archive/master.zip)
|
||||
of this repo and unpack it into /var/lib/grafana/plugins directory.
|
||||
|
||||
|
||||
## Motivation
|
||||
|
||||
We had a desperate need to show a set of timeseries statuses over time period, so we can see
|
||||
a history of changes for objects' statuses. Since we maintain a lot of Kubernetes clusters
|
||||
We had a desperate need to visualize a set of timeseries statuses over time period, so we can
|
||||
see a history of changes for objects' status. Since we maintain a lot of Kubernetes clusters
|
||||
(and related infrastructure), our main cases for that are visualization of servers & Kubernetes
|
||||
pods health states as well as HTTP services health checks. We've tried a variety of Grafana
|
||||
plugins available (they are listed in *Acknowledgements* below) but none of them could provide
|
||||
the features and visualization really close to what we've been looking for.
|
||||
|
||||
_NB: You can find more details about our journey of creating the plugin in
|
||||
[this post](https://medium.com/flant-com/statusmap-grafana-plugin-to-visualize-status-over-time-fe6ced391853)._
|
||||
|
||||
_Objects_ being visualized with this plugin may be different: not only IT components (e.g. server
|
||||
hosts and Kubernetes pods) but just anything you can imagine like coffee makers on the picture
|
||||
above. These objects should have _discrete statuses_ which are sets of predefined values, e.g.
|
||||
@@ -42,9 +57,20 @@ above. These objects should have _discrete statuses_ which are sets of predefine
|
||||
|
||||
## Configuration
|
||||
|
||||
### Prometheus
|
||||
### Datasource notes
|
||||
|
||||
To work with data from Prometheus you will need to setup discrete statuses of your objects.
|
||||
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.
|
||||
Requirements to store these statuses in metrics are as follows:
|
||||
* metrics should have two values: `0` and `1`;
|
||||
* there should be a label with status' value.
|
||||
@@ -58,8 +84,8 @@ When it's done, you can collect all the data via query, e.g.:
|
||||
If there was no such status (`<STATUS_VALUE>`) during query's interval, Prometheus would
|
||||
return nothing. Otherwise, status' value will be returned.
|
||||
|
||||
For example, if you have 5 statuses and a metric (`coffee_maker_status`) with 5 allowed
|
||||
values (`0`, `1`, `2`, `3`, `4`), you should transform this metric using the following rule:
|
||||
For example, if you have 5 types of statuses and a metric (`coffee_maker_status`) with 5
|
||||
allowed values (`0`, `1`, `2`, `3`, `4`), you should transform this metric using following rule:
|
||||
|
||||
```
|
||||
- record: coffee_maker_status:discrete
|
||||
@@ -76,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
|
||||
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
|
||||
|
||||
First of all, an individual query for each possible status' value should be created.
|
||||
@@ -87,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._
|
||||
|
||||
|
||||
### More options
|
||||
|
||||
#### Bucket
|
||||
|
||||

|
||||
|
||||
__Multiple values__ checkbox specifies how they should be displayed:
|
||||
@@ -107,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.
|
||||
__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
|
||||
|
||||
@@ -135,9 +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.
|
||||
|
||||
#### Changelog
|
||||
|
||||
##### 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
|
||||
|
||||
@@ -2,74 +2,186 @@
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Group values into rows and buckets by query's legend
|
||||
* Grouping values into rows and buckets using legend from query
|
||||
* User defined color mapping
|
||||
* Multiple values in bucket display in tooltip
|
||||
* Interval shaping to better visual representation
|
||||
* Represent null values as empty bucket or as zero value
|
||||
* Multiple values in bucket are displayed via tooltip
|
||||
* Increasing rows/buckets' interval for better visual representation
|
||||
* 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
|
||||
|
||||
* Prometheus datasource
|
||||
* Tested with Grafana 5.1.3
|
||||
* Tested with datasources:
|
||||
- Prometheus
|
||||
- InfluxDB
|
||||
- Mysql
|
||||
* Tested with Grafana:
|
||||
- 5.1.3
|
||||
- 5.4.3
|
||||
- 6.0.0
|
||||
|
||||
## Installation
|
||||
|
||||
Plugin can be installed with git:
|
||||
Plugin can be installed with GF_INSTALL_PLUGINS="flant-statusmap-panel" or you can use Git and clone this repo:
|
||||
|
||||
```
|
||||
git clone git@github.com:flant/grafana-statusmap.git /var/lib/grafana/plugins/flant-statusmap-panel
|
||||
```
|
||||
|
||||
Or you can download ZIP archive of this repo and unpack it into /var/lib/grafana/plugins directory.
|
||||
Alternatively, you can download [ZIP archive](https://github.com/flant/grafana-statusmap/archive/master.zip)
|
||||
of this repo and unpack it into /var/lib/grafana/plugins directory.
|
||||
|
||||
|
||||
## Motivation
|
||||
|
||||
This plugin emerges from our needs to visually represent history of changes for a set of objects
|
||||
with discrete statuses.
|
||||
_Objects_ can be hosts, Kubernetes pods or coffee makers and _discrete statuses_ are a set
|
||||
of predefined values: something like `ok` = 0, `off` = 1, `fail` = 2.
|
||||
We had a desperate need to visualize a set of timeseries statuses over time period, so we can
|
||||
see a history of changes for objects' status. Since we maintain a lot of Kubernetes clusters
|
||||
(and related infrastructure), our main cases for that are visualization of servers & Kubernetes
|
||||
pods health states as well as HTTP services health checks. We've tried a variety of Grafana
|
||||
plugins available (they are listed in *Acknowledgements* below) but none of them could provide
|
||||
the features and visualization really close to what we've been looking for.
|
||||
|
||||
_NB: You can find more details about our journey of creating the plugin in
|
||||
[this post](https://medium.com/flant-com/statusmap-grafana-plugin-to-visualize-status-over-time-fe6ced391853)._
|
||||
|
||||
_Objects_ being visualized with this plugin may be different: not only IT components (e.g. server
|
||||
hosts and Kubernetes pods) but just anything you can imagine like coffee makers on the picture
|
||||
above. These objects should have _discrete statuses_ which are sets of predefined values, e.g.
|
||||
`ok` = 0, `off` = 1, `fail` = 2, etc.
|
||||
|
||||
## Configuration
|
||||
|
||||
Recommended setup is to create query for each possible status value with similar legend:
|
||||
### 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.
|
||||
Requirements to store these statuses in metrics are as follows:
|
||||
* metrics should have two values: `0` and `1`;
|
||||
* there should be a label with status' value.
|
||||
|
||||
When it's done, you can collect all the data via query, e.g.:
|
||||
|
||||
```
|
||||
(max_over_time(coffee_maker_status{status="<STATUS_VALUE>"}[$__interval]) == 1) * <STATUS_VALUE>
|
||||
```
|
||||
|
||||
If there was no such status (`<STATUS_VALUE>`) during query's interval, Prometheus would
|
||||
return nothing. Otherwise, status' value will be returned.
|
||||
|
||||
For example, if you have 5 types of statuses and a metric (`coffee_maker_status`) with 5
|
||||
allowed values (`0`, `1`, `2`, `3`, `4`), you should transform this metric using following rule:
|
||||
|
||||
```
|
||||
- record: coffee_maker_status:discrete
|
||||
expr: |
|
||||
count_values("status", coffee_maker_status)
|
||||
```
|
||||
|
||||
That's how `coffee_maker_status` metric with value `3` will be transformed into new metric:
|
||||
|
||||
```
|
||||
coffee_maker_status:discrete{status="3"} 1
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### 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
|
||||
|
||||
First of all, an individual query for each possible status' value should be created.
|
||||
Each query should also have similar legend for grouping:
|
||||
|
||||

|
||||
|
||||
Next define color mapping for status values in __Discrete__ color mode.
|
||||
Then, color mapping for status' values should be defined in __Discrete__ color mode:
|
||||
|
||||

|
||||
|
||||
Use can use presets to define a trafic light colors or 8 colors from [solarized](https://ethanschoonover.com/solarized/) palette:
|
||||
|
||||
__Spectrum__ and __Opacity__ color modes works as in a [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
|
||||
|
||||
#### Bucket
|
||||
|
||||

|
||||
|
||||
__Multiple values__ check determine multiple values display mode. If check is unset then multiple values
|
||||
for one bucket treated as error. If check is on then color for bucket determined
|
||||
by value with least index in color mapping.
|
||||
__Multiple values__ checkbox specifies how they should be displayed:
|
||||
* If it's off, multiple values for one bucket are treated as error;
|
||||
* If it's on, color for such bucket would be determined by the value having least index in color mapping.
|
||||
|
||||

|
||||
|
||||
__Null values__ can be treated as empty buckets or displayed as color of 0 value.
|
||||
__Null values__ can be treated as empty buckets or displayed with the color of `0` value.
|
||||
|
||||

|
||||
|
||||
__Min width__ and __spacing__ are determine minimal bucket width and spacing between buckets.
|
||||
__Rounding__ is for round edges.
|
||||
__Min width__ and __spacing__ are used to specify minimal bucket width and spacing between buckets.
|
||||
__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
|
||||
|
||||
The easy way to test and develop plugin is to run Grafana instance in docker with following command in the directory containing the plugin.
|
||||
This will expose the local plugin on your machine to the Grafana container.
|
||||
To test and improve the plugin you can run Grafana instance in Docker using following command (in
|
||||
the directory containing Statusmap plugin):
|
||||
|
||||
```
|
||||
docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \
|
||||
@@ -78,7 +190,8 @@ docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \
|
||||
grafana/grafana:5.1.3
|
||||
```
|
||||
|
||||
Now run `grunt` to compile dist directory and start changes watcher:
|
||||
This will expose local plugin from your machine to Grafana container. Now run `grunt` to compile
|
||||
dist directory and start changes watcher:
|
||||
|
||||
```
|
||||
grunt watch
|
||||
@@ -86,13 +199,11 @@ grunt watch
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Idea of a plugin comes from Dmitry Stolyarov @distol, initial version written by Sergey Gnuskov @gsmetal and final changes made by Ivan Mikheykin @diafour.
|
||||
The first public release of this plugin has been fully made by [Flant](https://flant.com/) engineers. The whole idea has come from Dmitry Stolyarov ([@distol](https://github.com/distol)), initial version has been written by Sergey Gnuskov ([@gsmetal](https://github.com/gsmetal)) and final changes has been made by Ivan Mikheykin ([@diafour](https://github.com/diafour)).
|
||||
|
||||
This plugin is based on a "Heatmap" panel by Grafana and 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.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,33 +3,31 @@
|
||||
System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic/index', 'app/core/core', 'app/core/utils/ticks'], function (_export, _context) {
|
||||
"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) {
|
||||
var legendElem = $(elem).find('svg');
|
||||
var legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
var legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||
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 rangeStep = 1;
|
||||
if (rangeTo - rangeFrom > legendWidth) {
|
||||
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
||||
}
|
||||
|
||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
||||
}
|
||||
|
||||
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 valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||
|
||||
var colorScale = getColorScale(colorScheme, maxValue, minValue);
|
||||
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
||||
return d * widthFactor;
|
||||
}).attr("y", 0).attr("width", rangeStep * widthFactor + 1) // Overlap rectangles to prevent gaps
|
||||
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", 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;
|
||||
}).attr("y", 0)
|
||||
// rectangles are slightly overlaped to prevent gaps
|
||||
.attr("width", LEGEND_STEP_WIDTH + 1).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
|
||||
return colorScale(d);
|
||||
});
|
||||
|
||||
@@ -41,22 +39,24 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
||||
var legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
var legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||
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 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);
|
||||
|
||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
||||
}
|
||||
|
||||
var valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||
|
||||
var opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||
legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (d) {
|
||||
return d * widthFactor;
|
||||
}).attr("y", 0).attr("width", rangeStep * widthFactor).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", options.cardColor).style("opacity", 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;
|
||||
}).attr("y", 0)
|
||||
// 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);
|
||||
});
|
||||
|
||||
@@ -80,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
|
||||
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;
|
||||
}).each(function (d, i) {
|
||||
var thisWidth = this.getBBox().width;
|
||||
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]));
|
||||
legendElem.attr("width", legendWidth);
|
||||
@@ -178,22 +178,18 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
||||
|
||||
function drawSimpleColorLegend(elem, colorScale) {
|
||||
var legendElem = $(elem).find('svg');
|
||||
var legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
var legendWidth = Math.floor(legendElem.outerWidth());
|
||||
var legendHeight = legendElem.attr("height");
|
||||
|
||||
if (legendWidth) {
|
||||
var valuesNumber = Math.floor(legendWidth / 2);
|
||||
var rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||
var valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||
var valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||
|
||||
var legend = d3.select(legendElem.get(0));
|
||||
var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange);
|
||||
|
||||
legendRects.enter().append("rect").attr("x", function (d) {
|
||||
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect").attr("x", function (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) {
|
||||
return colorScale(d);
|
||||
});
|
||||
@@ -202,10 +198,9 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
||||
|
||||
function drawSimpleOpacityLegend(elem, options) {
|
||||
var legendElem = $(elem).find('svg');
|
||||
var graphElem = $(elem);
|
||||
var legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
var legend = d3.select(legendElem.get(0));
|
||||
var legendWidth = Math.floor(legendElem.outerWidth());
|
||||
var legendHeight = legendElem.attr("height");
|
||||
|
||||
@@ -217,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]);
|
||||
}
|
||||
|
||||
var rangeStep = 10;
|
||||
var valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||
var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange);
|
||||
var valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||
|
||||
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;
|
||||
}).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);
|
||||
});
|
||||
}
|
||||
@@ -329,11 +322,11 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
||||
}],
|
||||
execute: function () {
|
||||
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 () {
|
||||
return {
|
||||
@@ -367,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 () {
|
||||
return {
|
||||
@@ -384,6 +377,9 @@ System.register(['angular', 'lodash', 'jquery', 'd3', './libs/d3-scale-chromatic
|
||||
|
||||
function render() {
|
||||
clearLegend(elem);
|
||||
if (!ctrl.panel.legend.show) {
|
||||
return;
|
||||
}
|
||||
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
||||
var rangeFrom = ctrl.cardsData.minValue;
|
||||
var rangeTo = ctrl.cardsData.maxValue;
|
||||
|
||||
@@ -101,9 +101,33 @@ System.register(["lodash"], function (_export, _context) {
|
||||
value: function getBucketColor(values) {
|
||||
var thresholds = this.panel.color.thresholds;
|
||||
|
||||
for (var i = 0; i < thresholds.length; i++) {
|
||||
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] == thresholds[i].value) {
|
||||
if (values[j] != null) {
|
||||
isAllValuesNulls = false;
|
||||
}
|
||||
}
|
||||
if (isAllValuesNulls) {
|
||||
return this.getMatchedThreshold(null).color;
|
||||
}
|
||||
|
||||
for (var i = 0; i < thresholds.length; i++) {
|
||||
for (var _j = 0; _j < values.length; _j++) {
|
||||
if (values[_j] == thresholds[i].value) {
|
||||
return this.getDiscreteColor(i);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +159,7 @@ System.register(["lodash"], function (_export, _context) {
|
||||
key: "getMatchedThreshold",
|
||||
value: function getMatchedThreshold(value) {
|
||||
if (value == null) {
|
||||
if (this.panel.color.nullPointMode == 'as empty') {
|
||||
if (this.panel.nullPointMode == 'as empty') {
|
||||
// FIXME: make this explicit for user
|
||||
// Right now this color never used because null as empty handles in getCardOpacity method.
|
||||
return {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": "AAGA,8BAA+B;EAE7B,MAAM,EAAE,SAAS;EAEjB,uDAAyB;IACzB,gBAAgB,EAAE,KAAK;IACvB,OAAO,EAAE,GAAG;;AAId,qBAAsB;EACpB,QAAQ,EAAE,QAAQ;EAGhB,sCAAK;IACH,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,SAAS,EAAE,IAAI;EAGjB,sCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,OAAO;EAKnB,yBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,8BAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,uBAAwB;EACtB,WAAW,EAAE,MAAM;EACnB,SAAS,EAzCI,IAAI;EA0CjB,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;;AAGhB,8BAA+B;EAC7B,IAAI,EAAE,OAAO;;AAIb,8BAAK;EACH,MAAM,EAAE,OAAe;EACvB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAGlC,8BAA+B;EAE7B,MAAM,EAAE,MAAM;EAGd,kCAAI;IAGF,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,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,SAAS,EAAE,IAAI;EAGjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,OAAO;EAGjB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,EAAE,OAAO",
|
||||
"sources": ["../../src/css/status-heatmap.scss"],
|
||||
"names": [],
|
||||
"file": "status-heatmap.css"
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
.status-heatmap-canvas-wrapper {
|
||||
cursor: crosshair; }
|
||||
.status-heatmap-canvas-wrapper .datapoints-warning {
|
||||
pointer-events: none; }
|
||||
.status-heatmap-canvas-wrapper .datapoints-warning span {
|
||||
background-color: #333333;
|
||||
color: #d8d9da;
|
||||
padding: 1px; }
|
||||
|
||||
.status-heatmap-panel {
|
||||
position: relative; }
|
||||
.status-heatmap-panel .axis .tick text {
|
||||
fill: #d8d9da;
|
||||
color: #d8d9da;
|
||||
font-size: 11px; }
|
||||
.status-heatmap-panel .axis .tick line {
|
||||
opacity: 0.4;
|
||||
stroke: #8e8e8e; }
|
||||
.status-heatmap-panel svg {
|
||||
pointer-events: none; }
|
||||
.status-heatmap-panel svg rect {
|
||||
pointer-events: visiblePainted; }
|
||||
|
||||
.statusmap-tooltip {
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
background-color: #141414;
|
||||
color: #d8d9da; }
|
||||
.statusmap-tooltip .discrete-item {
|
||||
color: #52545c;
|
||||
padding: 1px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF; }
|
||||
|
||||
.statusmap-histogram rect {
|
||||
fill: #8e8e8e; }
|
||||
|
||||
.status-heatmap-crosshair line {
|
||||
stroke: #9f3023;
|
||||
stroke-width: 1; }
|
||||
|
||||
.status-heatmap-selection {
|
||||
stroke-width: 1;
|
||||
fill: rgba(102, 102, 102, 0.4);
|
||||
stroke: rgba(102, 102, 102, 0.8); }
|
||||
|
||||
.status-heatmap-legend-wrapper {
|
||||
margin: 0 10px; }
|
||||
.status-heatmap-legend-wrapper svg {
|
||||
height: 24px;
|
||||
float: left;
|
||||
white-space: nowrap; }
|
||||
.status-heatmap-legend-wrapper .status-heatmap-color-legend {
|
||||
margin-top: 4px; }
|
||||
.status-heatmap-legend-wrapper .status-heatmap-legend-values {
|
||||
display: inline-block; }
|
||||
.status-heatmap-legend-wrapper .axis .tick text {
|
||||
fill: #d8d9da;
|
||||
color: #d8d9da;
|
||||
font-size: 11px; }
|
||||
.status-heatmap-legend-wrapper .axis .tick line {
|
||||
opacity: 0.4;
|
||||
stroke: #8e8e8e; }
|
||||
.status-heatmap-legend-wrapper .axis .tick .domain {
|
||||
opacity: 0.4;
|
||||
stroke: #8e8e8e; }
|
||||
|
||||
/*# sourceMappingURL=statusmap.dark.css.map */
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"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"],
|
||||
"names": [],
|
||||
"file": "statusmap.dark.css"
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
.status-heatmap-canvas-wrapper {
|
||||
cursor: crosshair; }
|
||||
.status-heatmap-canvas-wrapper .datapoints-warning {
|
||||
pointer-events: none; }
|
||||
.status-heatmap-canvas-wrapper .datapoints-warning span {
|
||||
background-color: white;
|
||||
background-color: #e9edf2;
|
||||
color: #52545c;
|
||||
padding: 1px; }
|
||||
|
||||
.status-heatmap-panel {
|
||||
@@ -18,17 +21,22 @@
|
||||
.status-heatmap-panel svg rect {
|
||||
pointer-events: visiblePainted; }
|
||||
|
||||
.status-heatmap-tooltip {
|
||||
.statusmap-tooltip {
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
background-color: #dde4ed;
|
||||
color: #52545c; }
|
||||
.statusmap-tooltip .discrete-item {
|
||||
color: #52545c;
|
||||
padding: 1px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF; }
|
||||
|
||||
.status-heatmap-histogram rect {
|
||||
.statusmap-histogram rect {
|
||||
fill: #767980; }
|
||||
|
||||
.status-heatmap-crosshair line {
|
||||
stroke: #b30000;
|
||||
stroke: #9e2f22;
|
||||
stroke-width: 1; }
|
||||
|
||||
.status-heatmap-selection {
|
||||
@@ -57,4 +65,4 @@
|
||||
opacity: 0.4;
|
||||
stroke: #767980; }
|
||||
|
||||
/*# sourceMappingURL=status-heatmap.css.map */
|
||||
/*# sourceMappingURL=statusmap.light.css.map */
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"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"],
|
||||
"names": [],
|
||||
"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: 30 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 |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 64 KiB |
@@ -1,7 +1,7 @@
|
||||
<div class="status-heatmap-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.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
System.register(['./color_legend', './status_heatmap_ctrl'], function (_export, _context) {
|
||||
System.register(['app/plugins/sdk', './status_heatmap_ctrl'], function (_export, _context) {
|
||||
"use strict";
|
||||
|
||||
var StatusHeatmapCtrl;
|
||||
var loadPluginCss, StatusHeatmapCtrl;
|
||||
return {
|
||||
setters: [function (_color_legend) {}, function (_status_heatmap_ctrl) {
|
||||
setters: [function (_appPluginsSdk) {
|
||||
loadPluginCss = _appPluginsSdk.loadPluginCss;
|
||||
}, function (_status_heatmap_ctrl) {
|
||||
StatusHeatmapCtrl = _status_heatmap_ctrl.StatusHeatmapCtrl;
|
||||
}],
|
||||
execute: function () {
|
||||
|
||||
loadPluginCss({
|
||||
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
|
||||
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
|
||||
});
|
||||
|
||||
_export('PanelCtrl', StatusHeatmapCtrl);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../src/module.js"],"names":["loadPluginCss","StatusHeatmapCtrl","dark","light"],"mappings":";;;;;;;;AAAQA,mB,kBAAAA,a;;AACCC,uB,wBAAAA,iB;;;;AAETD,oBAAc;AACZE,cAAM,sDADM;AAEZC,eAAO;AAFK,OAAd;;2BAMEF,iB","file":"module.js","sourcesContent":["import {loadPluginCss} from 'app/plugins/sdk';\nimport { StatusHeatmapCtrl } from './status_heatmap_ctrl';\n\nloadPluginCss({\n dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',\n light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'\n});\n\nexport {\n StatusHeatmapCtrl as PanelCtrl\n};\n"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../src/options_editor.js"],"names":["statusHeatmapOptionsEditor","restrict","scope","templateUrl","controller","StatusHeatmapOptionsEditorCtrl","kbn","$scope","editor","panelCtrl","ctrl","panel","unitFormats","getUnitFormats","render","subItem","data","unitFormat","value"],"mappings":";;;;;;;;;;;;;AAkBO,WAASA,0BAAT,GAAsC;AAC3C;;AACA,WAAO;AACLC,gBAAU,GADL;AAELC,aAAO,IAFF;AAGLC,mBAAa,mEAHR;AAILC,kBAAYC;AAJP,KAAP;AAMD;;wCAReL,0B;;;;AAlBTM,S;;;;;;;;;;;;;;;;;;;;;gDAEMD,8B;AACX,gDAAYE,MAAZ,EAAoB;AAAA;;AAClBA,iBAAOC,MAAP,GAAgB,IAAhB;AACA,eAAKC,SAAL,GAAiBF,OAAOG,IAAxB;AACA,eAAKC,KAAL,GAAa,KAAKF,SAAL,CAAeE,KAA5B;AACA,eAAKC,WAAL,GAAmBN,IAAIO,cAAJ,EAAnB;;AAEA,eAAKJ,SAAL,CAAeK,MAAf;AACD;;;;wCAEaC,O,EAAS;AACrB,iBAAKJ,KAAL,CAAWK,IAAX,CAAgBC,UAAhB,GAA6BF,QAAQG,KAArC;AACA,iBAAKT,SAAL,CAAeK,MAAf;AACD","file":"options_editor.js","sourcesContent":["import kbn from 'app/core/utils/kbn';\n\nexport class StatusHeatmapOptionsEditorCtrl {\n constructor($scope) {\n $scope.editor = this;\n this.panelCtrl = $scope.ctrl;\n this.panel = this.panelCtrl.panel;\n this.unitFormats = kbn.getUnitFormats();\n\n this.panelCtrl.render();\n }\n\n setUnitFormat(subItem) {\n this.panel.data.unitFormat = subItem.value;\n this.panelCtrl.render();\n }\n}\n\nexport function statusHeatmapOptionsEditor() {\n 'use strict';\n return {\n restrict: 'E',\n scope: true,\n templateUrl: 'public/plugins/flant-statusmap-panel/partials/options_editor.html',\n controller: StatusHeatmapOptionsEditorCtrl,\n };\n}\n"]}
|
||||
@@ -55,40 +55,70 @@
|
||||
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
||||
</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-input width-30">No statuses defined. Add new status or use presets below.</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-2">{{ $index }}</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>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9 query-keyword" ng-if="ctrl.panel.useMax">If bucket has value</label>
|
||||
<label class="gf-form-label width-9 query-keyword" 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">
|
||||
<label class="gf-form-label query-keyword">
|
||||
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>
|
||||
</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" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
||||
<i class="fa fa-trash"/>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
|
||||
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
|
||||
Add trafic lights
|
||||
</a>
|
||||
</label>
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
|
||||
Add new status
|
||||
</a>
|
||||
</label>
|
||||
</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 class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
||||
@@ -118,6 +148,15 @@
|
||||
label="Show tooltip"
|
||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<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">
|
||||
@@ -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>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">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>
|
||||
<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="'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 class="gf-form">
|
||||
<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>
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
|
||||
],
|
||||
"screenshots": [
|
||||
{"name": "Panel", "path": "img/flant-statusmap-panel.png"},
|
||||
{"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"},
|
||||
{"name": "Panel with dark theme", "path": "img/flant-statusmap-panel-dark.png"},
|
||||
{"name": "Color mapping", "path": "img/color-mapping.png"},
|
||||
{"name": "Queries example", "path": "img/queries-example.png"}
|
||||
],
|
||||
"version": "0.0.1",
|
||||
"updated": "2018-09-17"
|
||||
"version": "0.1.1",
|
||||
"updated": "2019-03-22"
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'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";
|
||||
|
||||
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) {
|
||||
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
|
||||
var $heatmap = elem.find('.status-heatmap-panel');
|
||||
var tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
||||
var annotationTooltip = new AnnotationTooltip($heatmap, scope);
|
||||
|
||||
var width = void 0,
|
||||
height = void 0,
|
||||
@@ -26,7 +27,8 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
chartBottom = void 0,
|
||||
yAxisWidth = void 0,
|
||||
xAxisHeight = void 0,
|
||||
cardSpacing = void 0,
|
||||
cardVSpacing = void 0,
|
||||
cardHSpacing = void 0,
|
||||
cardRound = void 0,
|
||||
cardWidth = 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 max_text_width;
|
||||
return Math.ceil(max_text_width);
|
||||
}
|
||||
|
||||
function getXAxisHeight(elem) {
|
||||
@@ -121,9 +123,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
function getYScale(ticks) {
|
||||
var range = [];
|
||||
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++) {
|
||||
range.push(chartHeight - step * i);
|
||||
range.push(step * (i + 1));
|
||||
}
|
||||
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) {
|
||||
var range = [];
|
||||
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++) {
|
||||
range.push(chartHeight - step * i - yOffset);
|
||||
range.push(step * i + yOffset);
|
||||
}
|
||||
return d3.scaleOrdinal().domain(ticks).range(range);
|
||||
}
|
||||
@@ -149,6 +153,16 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
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);
|
||||
scope.yScale = yScale = getYScale(ticks);
|
||||
|
||||
@@ -216,12 +230,13 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
chartTop = margin.top;
|
||||
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;
|
||||
|
||||
// calculate yOffset for YAxis
|
||||
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
||||
cardHeight = yGridSize ? yGridSize - cardSpacing : 0;
|
||||
cardHeight = yGridSize ? yGridSize - cardVSpacing : 0;
|
||||
yOffset = cardHeight / 2;
|
||||
|
||||
addYAxis();
|
||||
@@ -229,9 +244,10 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||
chartWidth = width - yAxisWidth - margin.right;
|
||||
|
||||
// TODO allow per-y cardWidth!
|
||||
// we need to fill chartWidth with xBucketSize cards.
|
||||
xGridSize = chartWidth / (cardsData.xBucketSize + 1);
|
||||
cardWidth = xGridSize - cardSpacing;
|
||||
cardWidth = xGridSize - cardHSpacing;
|
||||
|
||||
addXAxis();
|
||||
xAxisHeight = getXAxisHeight(heatmap);
|
||||
@@ -274,6 +290,8 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
resetCardHighLight(event);
|
||||
});
|
||||
|
||||
_renderAnnotations();
|
||||
|
||||
ctrl.events.emit('render-complete', {
|
||||
"chartWidth": chartWidth
|
||||
});
|
||||
@@ -321,7 +339,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
var cx = xScale(d.x);
|
||||
|
||||
if (cx - cardWidth / 2 < 0) {
|
||||
x = yAxisWidth + cardSpacing / 2;
|
||||
x = yAxisWidth + cardHSpacing / 2;
|
||||
} else {
|
||||
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) {
|
||||
// Center should not exceed half of card.
|
||||
// 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;
|
||||
} else if (chartWidth - cx < cardWidth / 2) {
|
||||
// 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 {
|
||||
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
|
||||
w = Math.max(w, MIN_CARD_SIZE);
|
||||
|
||||
if (cardHSpacing == 0) {
|
||||
w = w + 1;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
function getCardY(d) {
|
||||
return yScale(d.y) + chartTop - cardHeight - cardSpacing / 2;
|
||||
return yScale(d.y) + chartTop - cardHeight - cardVSpacing / 2;
|
||||
}
|
||||
|
||||
function getCardHeight(d) {
|
||||
var ys = yScale(d.y);
|
||||
var y = ys + chartTop - cardHeight - cardSpacing / 2;
|
||||
var y = ys + chartTop - cardHeight - cardVSpacing / 2;
|
||||
var h = cardHeight;
|
||||
|
||||
// Cut card height to prevent overlay
|
||||
if (y < chartTop) {
|
||||
h = ys - cardSpacing / 2;
|
||||
h = ys - cardVSpacing / 2;
|
||||
} else if (ys > chartBottom) {
|
||||
h = chartBottom - y;
|
||||
} 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
|
||||
h = Math.max(h, MIN_CARD_SIZE);
|
||||
|
||||
if (cardVSpacing == 0) {
|
||||
h = h + 1;
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
@@ -452,6 +478,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
function onMouseLeave() {
|
||||
appEvents.emit('graph-hover-clear');
|
||||
clearCrosshair();
|
||||
//annotationTooltip.destroy();
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
@@ -463,6 +490,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
// Clear crosshair and tooltip
|
||||
clearCrosshair();
|
||||
tooltip.destroy();
|
||||
annotationTooltip.destroy();
|
||||
|
||||
selection.x2 = limitSelection(event.offsetX);
|
||||
drawSelection(selection.x1, selection.x2);
|
||||
@@ -470,6 +498,7 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
emitGraphHoverEvet(event);
|
||||
drawCrosshair(event.offsetX);
|
||||
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("mousemove", onMouseMove);
|
||||
$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);
|
||||
@@ -622,10 +694,13 @@ System.register(['lodash', 'jquery', 'moment', 'app/core/utils/kbn', 'app/core/c
|
||||
d3ScaleChromatic = _libsD3ScaleChromaticIndex;
|
||||
}, function (_tooltip) {
|
||||
StatusHeatmapTooltip = _tooltip.StatusHeatmapTooltip;
|
||||
}, function (_annotations) {
|
||||
AnnotationTooltip = _annotations.AnnotationTooltip;
|
||||
}],
|
||||
execute: function () {
|
||||
MIN_CARD_SIZE = 5;
|
||||
CARD_SPACING = 2;
|
||||
CARD_H_SPACING = 2;
|
||||
CARD_V_SPACING = 2;
|
||||
CARD_ROUND = 0;
|
||||
DATA_RANGE_WIDING_FACTOR = 1.2;
|
||||
DEFAULT_X_TICK_SIZE_PX = 100;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/kbn', './rendering', './options_editor', './color_mode_discrete', './css/status-heatmap.css!'], 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";
|
||||
|
||||
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) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
@@ -44,14 +44,39 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
contextSrv = _appCoreCore.contextSrv;
|
||||
}, function (_appCoreUtilsKbn) {
|
||||
kbn = _appCoreUtilsKbn.default;
|
||||
}, function (_rendering) {
|
||||
}, function (_color_legend) {}, function (_rendering) {
|
||||
rendering = _rendering.default;
|
||||
}, function (_options_editor) {
|
||||
statusHeatmapOptionsEditor = _options_editor.statusHeatmapOptionsEditor;
|
||||
}, function (_color_mode_discrete) {
|
||||
ColorModeDiscrete = _color_mode_discrete.ColorModeDiscrete;
|
||||
}, function (_cssStatusHeatmapCss) {}],
|
||||
}],
|
||||
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';
|
||||
SVG = 'SVG';
|
||||
VALUE_INDEX = 0;
|
||||
@@ -71,7 +96,8 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
},
|
||||
cards: {
|
||||
cardMinWidth: 5,
|
||||
cardSpacing: 2,
|
||||
cardVSpacing: 2,
|
||||
cardHSpacing: 2,
|
||||
cardRound: null
|
||||
},
|
||||
xAxis: {
|
||||
@@ -97,6 +123,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
},
|
||||
// how null points should be handled
|
||||
nullPointMode: 'as empty',
|
||||
yAxisSort: 'metrics',
|
||||
highlightCards: true,
|
||||
useMax: true
|
||||
};
|
||||
@@ -109,20 +136,22 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
{ name: 'Blues', value: 'interpolateBlues', invert: 'dark' }, { name: 'Greens', value: 'interpolateGreens', invert: 'dark' }, { name: 'Greys', value: 'interpolateGreys', invert: 'dark' }, { name: 'Oranges', value: 'interpolateOranges', invert: 'dark' }, { name: 'Purples', value: 'interpolatePurples', invert: 'dark' }, { name: 'Reds', value: 'interpolateReds', invert: 'dark' },
|
||||
|
||||
// Sequential (Multi-Hue)
|
||||
{ name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' }, { name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' }, { name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' }, { name: 'OrRd', value: 'interpolateOrRd', invert: 'dark' }, { name: 'PuBuGn', value: 'interpolatePuBuGn', invert: 'dark' }, { name: 'PuBu', value: 'interpolatePuBu', invert: 'dark' }, { name: 'PuRd', value: 'interpolatePuRd', invert: 'dark' }, { name: 'RdPu', value: 'interpolateRdPu', invert: 'dark' }, { name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' }, { name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' }, { name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' }, { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }];
|
||||
{ name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' }, { name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' }, { name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' }, { name: 'OrRd', value: 'interpolateOrRd', invert: 'dark' }, { name: 'PuBuGn', value: 'interpolatePuBuGn', invert: 'dark' }, { name: 'PuBu', value: 'interpolatePuBu', invert: 'dark' }, { name: 'PuRd', value: 'interpolatePuRd', invert: 'dark' }, { name: 'RdPu', value: 'interpolateRdPu', invert: 'dark' }, { name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' }, { name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' }, { name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' }, { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' }];
|
||||
colorModes = ['opacity', 'spectrum', 'discrete'];
|
||||
opacityScales = ['linear', 'sqrt'];
|
||||
|
||||
_export('StatusHeatmapCtrl', StatusHeatmapCtrl = function (_MetricsPanelCtrl) {
|
||||
_inherits(StatusHeatmapCtrl, _MetricsPanelCtrl);
|
||||
|
||||
function StatusHeatmapCtrl($scope, $injector, $rootScope, timeSrv) {
|
||||
/** @ngInject */
|
||||
function StatusHeatmapCtrl($scope, $injector, $rootScope, timeSrv, annotationsSrv) {
|
||||
_classCallCheck(this, StatusHeatmapCtrl);
|
||||
|
||||
var _this = _possibleConstructorReturn(this, (StatusHeatmapCtrl.__proto__ || Object.getPrototypeOf(StatusHeatmapCtrl)).call(this, $scope, $injector));
|
||||
|
||||
_this.onRenderComplete = function (data) {
|
||||
_this.graph.chartWidth = data.chartWidth;
|
||||
_this.renderingCompleted();
|
||||
};
|
||||
|
||||
_this.calculateInterval = function () {
|
||||
@@ -131,7 +160,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
var chartWidth = _.max([panelWidth - 200, panelWidth / 2]);
|
||||
|
||||
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 intervalMs = void 0;
|
||||
@@ -166,11 +195,49 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
_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.data = dataList;
|
||||
_this.cardsData = _this.convertToCards(_this.data);
|
||||
|
||||
_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 () {
|
||||
@@ -206,6 +273,7 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
|
||||
_this.onDataError = function () {
|
||||
_this.data = [];
|
||||
_this.annotations = [];
|
||||
_this.render();
|
||||
};
|
||||
|
||||
@@ -223,6 +291,11 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
_this.render();
|
||||
};
|
||||
|
||||
_this.onEditorRemoveThresholds = function () {
|
||||
_this.panel.color.thresholds = [];
|
||||
_this.render();
|
||||
};
|
||||
|
||||
_this.onEditorAddThreeLights = function () {
|
||||
_this.panel.color.thresholds.push({ color: "red", value: 2, tooltip: "error" });
|
||||
_this.panel.color.thresholds.push({ color: "yellow", value: 1, tooltip: "warning" });
|
||||
@@ -230,6 +303,18 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
_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) {
|
||||
rendering(scope, elem, attrs, ctrl);
|
||||
};
|
||||
@@ -242,51 +327,54 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
maxValue: 0,
|
||||
minValue: 0,
|
||||
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;
|
||||
}
|
||||
|
||||
// collect uniq targets and their indexes in data array
|
||||
cardsData.targetIndex = {};
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var ts = data[i];
|
||||
var target = ts.target;
|
||||
if (cardsData.targetIndex[target] == undefined) {
|
||||
cardsData.targetIndex[target] = [];
|
||||
}
|
||||
cardsData.targetIndex[target].push(i);
|
||||
}
|
||||
// Collect uniq timestamps from data and spread over targets and timestamps
|
||||
|
||||
// collect uniq targets and their indices
|
||||
_.map(data, function (d, i) {
|
||||
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i);
|
||||
});
|
||||
|
||||
// TODO add some logic for targets heirarchy
|
||||
cardsData.targets = _.keys(cardsData.targetIndex);
|
||||
cardsData.targets.sort();
|
||||
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;
|
||||
}));
|
||||
|
||||
// Collect all values for each bucket from datapoints with similar target.
|
||||
for (var _i = 0; _i < cardsData.targets.length; _i++) {
|
||||
var _target = cardsData.targets[_i];
|
||||
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
|
||||
for (var i = 0; i < cardsData.targets.length; i++) {
|
||||
var target = cardsData.targets[i];
|
||||
|
||||
for (var j = 0; j < cardsData.xBucketSize; j++) {
|
||||
var card = {
|
||||
id: _i * cardsData.xBucketSize + j,
|
||||
id: i * cardsData.xBucketSize + j,
|
||||
values: [],
|
||||
multipleValues: false,
|
||||
noColorDefined: false
|
||||
noColorDefined: false,
|
||||
y: target,
|
||||
x: -1
|
||||
};
|
||||
|
||||
// collect values from all timeseries with target
|
||||
for (var si = 0; si < cardsData.targetIndex[_target].length; si++) {
|
||||
var s = data[cardsData.targetIndex[_target][si]];
|
||||
for (var si = 0; si < cardsData.targetIndex[target].length; si++) {
|
||||
var s = data[cardsData.targetIndex[target][si]];
|
||||
if (s.datapoints.length <= j) {
|
||||
continue;
|
||||
}
|
||||
var datapoint = s.datapoints[j];
|
||||
if (card.values.length === 0) {
|
||||
card.x = datapoint[TIME_INDEX];
|
||||
card.y = s.target;
|
||||
}
|
||||
card.values.push(datapoint[VALUE_INDEX]);
|
||||
}
|
||||
@@ -304,9 +392,11 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
|
||||
if (cardsData.minValue > card.minValue) cardsData.minValue = card.minValue;
|
||||
|
||||
if (card.x != -1) {
|
||||
cardsData.cards.push(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cardsData;
|
||||
};
|
||||
@@ -338,12 +428,16 @@ 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-snapshot-load', _this.onDataReceived);
|
||||
_this.events.on('data-error', _this.onDataError);
|
||||
_this.events.on('init-edit-mode', _this.onInitEditMode);
|
||||
_this.events.on('render', _this.onRender);
|
||||
_this.events.on('refresh', _this.postRefresh);
|
||||
// custom event from rendering.js
|
||||
_this.events.on('render-complete', _this.onRenderComplete);
|
||||
return _this;
|
||||
}
|
||||
@@ -351,6 +445,9 @@ System.register(['app/plugins/sdk', 'lodash', 'app/core/core', 'app/core/utils/k
|
||||
// override calculateInterval for discrete color mode
|
||||
|
||||
|
||||
/* https://ethanschoonover.com/solarized/ */
|
||||
|
||||
|
||||
// group values into buckets by target
|
||||
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_exp
|
||||
}, {
|
||||
key: 'add',
|
||||
value: function add() {
|
||||
this.tooltip = d3.select("body").append("div").attr("class", "heatmap-tooltip graph-tooltip grafana-tooltip");
|
||||
this.tooltip = d3.select("body").append("div").attr("class", "statusmap-tooltip graph-tooltip grafana-tooltip");
|
||||
}
|
||||
}, {
|
||||
key: 'destroy',
|
||||
@@ -128,7 +128,7 @@ System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_exp
|
||||
var tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
var time = this.dashboard.formatDate(+x, tooltipTimeFormat);
|
||||
|
||||
var tooltipHtml = '<div class="graph-tooltip-time">' + time + '</div>\n <div class="status-heatmap-histogram"></div>';
|
||||
var tooltipHtml = '<div class="graph-tooltip-time">' + time + '</div>\n <div class="statusmap-histogram"></div>';
|
||||
|
||||
if (this.panel.color.mode === 'discrete') {
|
||||
var statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
|
||||
@@ -139,7 +139,7 @@ System.register(['d3', 'jquery', 'lodash', 'app/core/utils/kbn'], function (_exp
|
||||
statusesHtml = "statuses:";
|
||||
}
|
||||
tooltipHtml += '\n <div>\n name: <b>' + y + '</b> <br>\n ' + statusesHtml + '\n <ul>\n ' + _.join(_.map(statuses, function (v) {
|
||||
return '<li style="background-color: ' + v.color + '; padding: 1px; font-weight: bold; text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF">' + v.tooltip + '</li>';
|
||||
return '<li style="background-color: ' + v.color + '" class="discrete-item">' + v.tooltip + '</li>';
|
||||
}), "") + '\n </ul>\n </div>';
|
||||
} else {
|
||||
if (values.length === 1) {
|
||||
|
||||
@@ -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",
|
||||
"version": "0.0.1",
|
||||
"description": "Status Heatmap panel plugin for grafana",
|
||||
"name": "flant-statusmap-panel",
|
||||
"version": "0.1.0",
|
||||
"description": "Grafana panel plugin to visualize status of multiple objects over time",
|
||||
"main": "README.md",
|
||||
"scripts": {
|
||||
"build": "grunt"
|
||||
@@ -10,7 +10,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/flant/grafana-status-heatmap.git"
|
||||
"url": "https://github.com/flant/grafana-statusmap.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel": "^6.23.0",
|
||||
@@ -27,6 +27,7 @@
|
||||
"grunt-contrib-uglify": "^3.0.1",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-execute": "^0.2.2",
|
||||
"grunt-notify": "^0.4.5",
|
||||
"grunt-systemjs-builder": "^1.0.0",
|
||||
"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');
|
||||
|
||||
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() {
|
||||
return {
|
||||
@@ -45,7 +45,7 @@ mod.directive('optionsColorLegend', function() {
|
||||
});
|
||||
|
||||
/**
|
||||
* Heatmap legend with scale values.
|
||||
* Graph legend with values.
|
||||
*/
|
||||
mod.directive('statusHeatmapLegend', function() {
|
||||
return {
|
||||
@@ -62,6 +62,9 @@ mod.directive('statusHeatmapLegend', function() {
|
||||
|
||||
function render() {
|
||||
clearLegend(elem);
|
||||
if (!ctrl.panel.legend.show) {
|
||||
return
|
||||
}
|
||||
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) {
|
||||
let rangeFrom = ctrl.cardsData.minValue;
|
||||
let rangeTo = ctrl.cardsData.maxValue;
|
||||
@@ -89,18 +92,12 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
|
||||
let legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||
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 rangeStep = 1;
|
||||
if (rangeTo - rangeFrom > legendWidth) {
|
||||
rangeStep = Math.floor((rangeTo - rangeFrom) / legendWidth);
|
||||
}
|
||||
|
||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
||||
}
|
||||
|
||||
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 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")
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.attr("x", d => d * widthFactor)
|
||||
// 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("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("stroke-width", 0)
|
||||
.attr("fill", d => colorScale(d));
|
||||
@@ -123,25 +123,25 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
|
||||
let legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
let legendWidth = Math.floor(legendElem.outerWidth()) - 30;
|
||||
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 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);
|
||||
|
||||
if (rangeStep * MIN_LEGEND_STEPS > rangeTo) {
|
||||
rangeStep = rangeTo / MIN_LEGEND_STEPS;
|
||||
}
|
||||
|
||||
let valuesRange = d3.range(rangeFrom, rangeTo, rangeStep);
|
||||
|
||||
let opacityScale = getOpacityScale(options, maxValue, minValue);
|
||||
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.attr("x", d => d * widthFactor)
|
||||
// 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("width", rangeStep * widthFactor)
|
||||
// rectangles are slightly overlaped to prevent gaps
|
||||
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||
.attr("height", legendHeight)
|
||||
.attr("stroke-width", 0)
|
||||
.attr("fill", options.cardColor)
|
||||
@@ -168,14 +168,14 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteHelper) {
|
||||
legend.selectAll(".hidden-texts")
|
||||
.data(tooltips)
|
||||
.enter().append("text")
|
||||
.attr("class", "axis tick")
|
||||
.attr("class", "axis tick hidden-texts")
|
||||
.attr("font-family", "sans-serif")
|
||||
.text(d => d)
|
||||
.each(function(d,i) {
|
||||
let thisWidth = this.getBBox().width;
|
||||
textWidth.push(thisWidth);
|
||||
this.remove(); // remove them just after displaying them
|
||||
});
|
||||
legend.selectAll(".hidden-texts").remove();
|
||||
|
||||
let legendWidth = Math.floor(_.min([
|
||||
graphWidth - 30,
|
||||
@@ -289,23 +289,21 @@ function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
|
||||
|
||||
function drawSimpleColorLegend(elem, colorScale) {
|
||||
let legendElem = $(elem).find('svg');
|
||||
let legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||
let legendHeight = legendElem.attr("height");
|
||||
|
||||
if (legendWidth) {
|
||||
let valuesNumber = Math.floor(legendWidth / 2);
|
||||
let rangeStep = Math.floor(legendWidth / valuesNumber);
|
||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||
|
||||
let legend = d3.select(legendElem.get(0));
|
||||
var legendRects = legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange);
|
||||
|
||||
legendRects.enter().append("rect")
|
||||
legend.selectAll(".status-heatmap-color-legend-rect")
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.attr("x", d => d)
|
||||
.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("stroke-width", 0)
|
||||
.attr("fill", d => colorScale(d));
|
||||
@@ -314,10 +312,9 @@ function drawSimpleColorLegend(elem, colorScale) {
|
||||
|
||||
function drawSimpleOpacityLegend(elem, options) {
|
||||
let legendElem = $(elem).find('svg');
|
||||
let graphElem = $(elem);
|
||||
let legend = d3.select(legendElem.get(0));
|
||||
clearLegend(elem);
|
||||
|
||||
let legend = d3.select(legendElem.get(0));
|
||||
let legendWidth = Math.floor(legendElem.outerWidth());
|
||||
let legendHeight = legendElem.attr("height");
|
||||
|
||||
@@ -333,14 +330,14 @@ function drawSimpleOpacityLegend(elem, options) {
|
||||
.range([0, 1]);
|
||||
}
|
||||
|
||||
let rangeStep = 10;
|
||||
let valuesRange = d3.range(0, legendWidth, rangeStep);
|
||||
var legendRects = legend.selectAll(".status-heatmap-opacity-legend-rect").data(valuesRange);
|
||||
let valuesRange = d3.range(0, legendWidth, LEGEND_STEP_WIDTH);
|
||||
|
||||
legendRects.enter().append("rect")
|
||||
legend.selectAll(".status-heatmap-opacity-legend-rect")
|
||||
.data(valuesRange)
|
||||
.enter().append("rect")
|
||||
.attr("x", d => d)
|
||||
.attr("y", 0)
|
||||
.attr("width", rangeStep)
|
||||
.attr("width", LEGEND_STEP_WIDTH+1)
|
||||
.attr("height", legendHeight)
|
||||
.attr("stroke-width", 0)
|
||||
.attr("fill", options.cardColor)
|
||||
|
||||
@@ -61,6 +61,30 @@ export class ColorModeDiscrete {
|
||||
getBucketColor(values) {
|
||||
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 j = 0; j < values.length; j++) {
|
||||
if (values[j] == thresholds[i].value) {
|
||||
@@ -93,7 +117,7 @@ export class ColorModeDiscrete {
|
||||
|
||||
getMatchedThreshold(value) {
|
||||
if (value == null) {
|
||||
if (this.panel.color.nullPointMode == 'as empty') {
|
||||
if (this.panel.nullPointMode == 'as empty') {
|
||||
// FIXME: make this explicit for user
|
||||
// Right now this color never used because null as empty handles in getCardOpacity method.
|
||||
return {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
$font-size-sm: 12px !default;
|
||||
|
||||
|
||||
.status-heatmap-canvas-wrapper {
|
||||
// position: relative;
|
||||
cursor: crosshair;
|
||||
|
||||
// allow tooltips on hover of warning text
|
||||
.datapoints-warning {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.datapoints-warning span {
|
||||
background-color: white;
|
||||
background-color: $warning-bg;
|
||||
color: $text-color;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
@@ -16,14 +19,14 @@ $font-size-sm: 12px !default;
|
||||
|
||||
.axis .tick {
|
||||
text {
|
||||
fill: #52545c;
|
||||
color: #52545c;
|
||||
font-size: 11px;
|
||||
fill: $text-color;
|
||||
color: $text-color;
|
||||
font-size: $font-size-tick;
|
||||
}
|
||||
|
||||
line {
|
||||
opacity: 0.4;
|
||||
stroke: #767980;
|
||||
stroke: $text-color-weak;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,20 +40,27 @@ $font-size-sm: 12px !default;
|
||||
}
|
||||
}
|
||||
|
||||
.status-heatmap-tooltip {
|
||||
.statusmap-tooltip {
|
||||
white-space: nowrap;
|
||||
font-size: $font-size-sm;
|
||||
background-color: #dde4ed;
|
||||
background-color: $graph-tooltip-bg;
|
||||
color: $text-color;
|
||||
|
||||
.discrete-item {
|
||||
color: #52545c;
|
||||
padding: 1px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
.status-heatmap-histogram rect {
|
||||
fill: #767980;
|
||||
.statusmap-histogram rect {
|
||||
fill: $text-color-weak;
|
||||
}
|
||||
|
||||
.status-heatmap-crosshair {
|
||||
line {
|
||||
stroke: darken(red,15%);
|
||||
stroke: darken($red,15%);
|
||||
stroke-width: 1;
|
||||
}
|
||||
}
|
||||
@@ -61,14 +71,11 @@ $font-size-sm: 12px !default;
|
||||
stroke: rgba(102, 102, 102, 0.8);
|
||||
}
|
||||
|
||||
|
||||
.status-heatmap-legend-wrapper {
|
||||
// @include clearfix();
|
||||
margin: 0 10px;
|
||||
//padding-top: 10px;
|
||||
|
||||
svg {
|
||||
//width: 100%;
|
||||
//max-width: 1300px;
|
||||
height: 24px;
|
||||
float: left;
|
||||
white-space: nowrap;
|
||||
@@ -85,19 +92,19 @@ $font-size-sm: 12px !default;
|
||||
|
||||
.axis .tick {
|
||||
text {
|
||||
fill: #52545c;
|
||||
color: #52545c;
|
||||
font-size: 11px;
|
||||
fill: $text-color;
|
||||
color: $text-color;
|
||||
font-size: $font-size-tick;
|
||||
}
|
||||
|
||||
line {
|
||||
opacity: 0.4;
|
||||
stroke: #767980;
|
||||
stroke: $text-color-weak;
|
||||
}
|
||||
|
||||
.domain {
|
||||
opacity: 0.4;
|
||||
stroke: #767980;
|
||||
stroke: $text-color-weak;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Dark variables
|
||||
$text-color: #d8d9da; // $gray-4
|
||||
$text-color-weak: #8e8e8e; // $gray-2
|
||||
$graph-tooltip-bg: #141414; // $dark-1
|
||||
$warning-bg: #333333; // $dark-4
|
||||
|
||||
$theme-name: dark;
|
||||
$white: #fff;
|
||||
$red: #d44a3a;
|
||||
@@ -0,0 +1,10 @@
|
||||
// Light variables
|
||||
$text-color: #52545c; // $gray-1
|
||||
$text-color-weak: #767980; // $gray-2
|
||||
$graph-tooltip-bg: #dde4ed; // $gray-5
|
||||
$warning-bg: #e9edf2; // $gray-6
|
||||
|
||||
$theme-name: light;
|
||||
$white: #fff;
|
||||
$red: #d44939;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
$font-size-sm: 12px !default;
|
||||
$font-size-tick: 11px;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@import "variables";
|
||||
@import "variables.dark";
|
||||
@import "statusmap"
|
||||
@@ -0,0 +1,3 @@
|
||||
@import "variables";
|
||||
@import "variables.light";
|
||||
@import "statusmap"
|
||||
|
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: 30 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-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.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import './color_legend';
|
||||
import {loadPluginCss} from 'app/plugins/sdk';
|
||||
import { StatusHeatmapCtrl } from './status_heatmap_ctrl';
|
||||
|
||||
loadPluginCss({
|
||||
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
|
||||
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
|
||||
});
|
||||
|
||||
export {
|
||||
StatusHeatmapCtrl as PanelCtrl
|
||||
};
|
||||
|
||||
@@ -55,40 +55,70 @@
|
||||
<strong>Note:</strong> Multiple values displayed using color with least index<br/>
|
||||
</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-input width-30">No statuses defined. Add new status or use presets below.</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-repeat="threshold in ctrl.panel.color.thresholds">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-2">{{ $index }}</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>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9 query-keyword" ng-if="ctrl.panel.useMax">If bucket has value</label>
|
||||
<label class="gf-form-label width-9 query-keyword" 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">
|
||||
<label class="gf-form-label query-keyword">
|
||||
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>
|
||||
</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" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)">
|
||||
<i class="fa fa-trash"/>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label" ng-show="ctrl.panel.color.thresholds.length == 0">
|
||||
<a class="pointer" ng-click="ctrl.onEditorAddThreeLights()">
|
||||
Add trafic lights
|
||||
</a>
|
||||
</label>
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" ng-click="ctrl.onEditorAddThreshold()">
|
||||
Add new status
|
||||
</a>
|
||||
</label>
|
||||
</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 class="gf-form" ng-show="ctrl.panel.color.mode !== 'discrete'">
|
||||
@@ -118,6 +148,15 @@
|
||||
label="Show tooltip"
|
||||
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<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">
|
||||
@@ -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>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">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>
|
||||
<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="'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 class="gf-form">
|
||||
<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>
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
|
||||
],
|
||||
"screenshots": [
|
||||
{"name": "Panel", "path": "img/flant-statusmap-panel.png"},
|
||||
{"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"},
|
||||
{"name": "Panel with dark theme", "path": "img/flant-statusmap-panel-dark.png"},
|
||||
{"name": "Color mapping", "path": "img/color-mapping.png"},
|
||||
{"name": "Queries example", "path": "img/queries-example.png"}
|
||||
],
|
||||
"version": "0.0.1",
|
||||
"updated": "2018-09-17"
|
||||
"version": "0.1.1",
|
||||
"updated": "2019-03-22"
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
|
||||
@@ -7,9 +7,11 @@ import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks
|
||||
import d3 from 'd3';
|
||||
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
|
||||
import {StatusHeatmapTooltip} from './tooltip';
|
||||
import {AnnotationTooltip} from './annotations';
|
||||
|
||||
let MIN_CARD_SIZE = 5,
|
||||
CARD_SPACING = 2,
|
||||
CARD_H_SPACING = 2,
|
||||
CARD_V_SPACING = 2,
|
||||
CARD_ROUND = 0,
|
||||
DATA_RANGE_WIDING_FACTOR = 1.2,
|
||||
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
|
||||
let $heatmap = elem.find('.status-heatmap-panel');
|
||||
let tooltip = new StatusHeatmapTooltip($heatmap, scope);
|
||||
let annotationTooltip = new AnnotationTooltip($heatmap, scope);
|
||||
|
||||
let width, height,
|
||||
yScale, xScale,
|
||||
chartWidth, chartHeight,
|
||||
chartTop, chartBottom,
|
||||
yAxisWidth, xAxisHeight,
|
||||
cardSpacing, cardRound,
|
||||
cardVSpacing, cardHSpacing, cardRound,
|
||||
cardWidth, cardHeight,
|
||||
colorScale, opacityScale,
|
||||
mouseUpHandler,
|
||||
@@ -76,7 +79,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
return text.getBBox().width;
|
||||
}));
|
||||
|
||||
return max_text_width;
|
||||
return Math.ceil(max_text_width);
|
||||
}
|
||||
|
||||
function getXAxisHeight(elem) {
|
||||
@@ -129,9 +132,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
function getYScale(ticks) {
|
||||
let range = [];
|
||||
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++) {
|
||||
range.push(chartHeight - step * i);
|
||||
range.push(step * (i+1));
|
||||
}
|
||||
return d3.scaleOrdinal()
|
||||
.domain(ticks)
|
||||
@@ -142,9 +146,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
function getYAxisScale(ticks) {
|
||||
let range = [];
|
||||
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++) {
|
||||
range.push(chartHeight - step * i - yOffset);
|
||||
range.push(step * i + yOffset);
|
||||
}
|
||||
return d3.scaleOrdinal()
|
||||
.domain(ticks)
|
||||
@@ -159,6 +164,12 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
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);
|
||||
scope.yScale = yScale = getYScale(ticks);
|
||||
|
||||
@@ -231,12 +242,13 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
chartTop = margin.top;
|
||||
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;
|
||||
|
||||
// calculate yOffset for YAxis
|
||||
yGridSize = Math.floor(chartHeight / cardsData.yBucketSize);
|
||||
cardHeight = yGridSize ? yGridSize - cardSpacing : 0;
|
||||
cardHeight = yGridSize ? yGridSize - cardVSpacing : 0;
|
||||
yOffset = cardHeight / 2;
|
||||
|
||||
addYAxis();
|
||||
@@ -244,9 +256,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
|
||||
chartWidth = width - yAxisWidth - margin.right;
|
||||
|
||||
// TODO allow per-y cardWidth!
|
||||
// we need to fill chartWidth with xBucketSize cards.
|
||||
xGridSize = chartWidth / (cardsData.xBucketSize+1);
|
||||
cardWidth = xGridSize - cardSpacing;
|
||||
cardWidth = xGridSize - cardHSpacing;
|
||||
|
||||
addXAxis();
|
||||
xAxisHeight = getXAxisHeight(heatmap);
|
||||
@@ -299,6 +312,8 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
resetCardHighLight(event);
|
||||
});
|
||||
|
||||
_renderAnnotations();
|
||||
|
||||
ctrl.events.emit('render-complete', {
|
||||
"chartWidth": chartWidth
|
||||
});
|
||||
@@ -356,7 +371,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
let cx = xScale(d.x);
|
||||
|
||||
if (cx - cardWidth/2 < 0) {
|
||||
x = yAxisWidth + cardSpacing/2;
|
||||
x = yAxisWidth + cardHSpacing/2;
|
||||
} else {
|
||||
x = yAxisWidth + cx - cardWidth/2;
|
||||
}
|
||||
@@ -372,11 +387,11 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
if (cx < cardWidth/2) {
|
||||
// Center should not exceed half of card.
|
||||
// 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;
|
||||
} else if (chartWidth - cx < cardWidth/2) {
|
||||
// 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 {
|
||||
w = cardWidth;
|
||||
}
|
||||
@@ -384,21 +399,25 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
// Card width should be MIN_CARD_SIZE at least
|
||||
w = Math.max(w, MIN_CARD_SIZE);
|
||||
|
||||
if (cardHSpacing == 0) {
|
||||
w = w+1;
|
||||
}
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
function getCardY(d) {
|
||||
return yScale(d.y) + chartTop - cardHeight - cardSpacing/2;
|
||||
return yScale(d.y) + chartTop - cardHeight - cardVSpacing/2;
|
||||
}
|
||||
|
||||
function getCardHeight(d) {
|
||||
let ys = yScale(d.y);
|
||||
let y = ys + chartTop - cardHeight - cardSpacing/2;
|
||||
let y = ys + chartTop - cardHeight - cardVSpacing/2;
|
||||
let h = cardHeight;
|
||||
|
||||
// Cut card height to prevent overlay
|
||||
if (y < chartTop) {
|
||||
h = ys - cardSpacing/2;
|
||||
h = ys - cardVSpacing/2;
|
||||
} else if (ys > chartBottom) {
|
||||
h = chartBottom - y;
|
||||
} 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
|
||||
h = Math.max(h, MIN_CARD_SIZE);
|
||||
|
||||
if (cardVSpacing == 0) {
|
||||
h = h+1
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
@@ -487,6 +510,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
function onMouseLeave() {
|
||||
appEvents.emit('graph-hover-clear');
|
||||
clearCrosshair();
|
||||
//annotationTooltip.destroy();
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
@@ -496,6 +520,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
// Clear crosshair and tooltip
|
||||
clearCrosshair();
|
||||
tooltip.destroy();
|
||||
annotationTooltip.destroy();
|
||||
|
||||
selection.x2 = limitSelection(event.offsetX);
|
||||
drawSelection(selection.x1, selection.x2);
|
||||
@@ -503,6 +528,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
emitGraphHoverEvet(event);
|
||||
drawCrosshair(event.offsetX);
|
||||
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("mousemove", onMouseMove);
|
||||
$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) {
|
||||
|
||||
@@ -3,13 +3,13 @@ import _ from 'lodash';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
import './color_legend';
|
||||
import rendering from './rendering';
|
||||
// import aggregates, { aggregatesMap } from './aggregates';
|
||||
// import fragments, { fragmentsMap } from './fragments';
|
||||
// import { labelFormats } from './xAxisLabelFormats';
|
||||
import {statusHeatmapOptionsEditor} from './options_editor';
|
||||
import {ColorModeDiscrete} from "./color_mode_discrete";
|
||||
import './css/status-heatmap.css!';
|
||||
|
||||
const CANVAS = 'CANVAS';
|
||||
const SVG = 'SVG';
|
||||
@@ -31,7 +31,8 @@ const panelDefaults = {
|
||||
},
|
||||
cards: {
|
||||
cardMinWidth: 5,
|
||||
cardSpacing: 2,
|
||||
cardVSpacing: 2,
|
||||
cardHSpacing: 2,
|
||||
cardRound: null
|
||||
},
|
||||
xAxis: {
|
||||
@@ -57,6 +58,7 @@ const panelDefaults = {
|
||||
},
|
||||
// how null points should be handled
|
||||
nullPointMode: 'as empty',
|
||||
yAxisSort: 'metrics',
|
||||
highlightCards: true,
|
||||
useMax: true
|
||||
};
|
||||
@@ -89,7 +91,7 @@ const colorSchemes = [
|
||||
{ name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' },
|
||||
{ name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' },
|
||||
{ name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' },
|
||||
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' }
|
||||
{ name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' }
|
||||
];
|
||||
|
||||
let colorModes = ['opacity', 'spectrum', 'discrete'];
|
||||
@@ -98,7 +100,8 @@ let opacityScales = ['linear', 'sqrt'];
|
||||
export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
static templateUrl = 'module.html';
|
||||
|
||||
constructor($scope, $injector, $rootScope, timeSrv) {
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, $rootScope, timeSrv, annotationsSrv) {
|
||||
super($scope, $injector);
|
||||
_.defaultsDeep(this.panel, panelDefaults);
|
||||
|
||||
@@ -127,17 +130,22 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
};
|
||||
|
||||
this.annotations = [];
|
||||
this.annotationsSrv = annotationsSrv;
|
||||
|
||||
this.events.on('data-received', this.onDataReceived);
|
||||
this.events.on('data-snapshot-load', this.onDataReceived);
|
||||
this.events.on('data-error', this.onDataError);
|
||||
this.events.on('init-edit-mode', this.onInitEditMode);
|
||||
this.events.on('render', this.onRender);
|
||||
this.events.on('refresh', this.postRefresh);
|
||||
// custom event from rendering.js
|
||||
this.events.on('render-complete', this.onRenderComplete);
|
||||
}
|
||||
|
||||
onRenderComplete = (data) => {
|
||||
this.graph.chartWidth = data.chartWidth;
|
||||
this.renderingCompleted();
|
||||
};
|
||||
|
||||
// override calculateInterval for discrete color mode
|
||||
@@ -150,7 +158,7 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
]);
|
||||
|
||||
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 intervalMs;
|
||||
@@ -185,11 +193,53 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
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) => {
|
||||
this.data = dataList;
|
||||
this.cardsData = this.convertToCards(this.data);
|
||||
|
||||
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 = () => {
|
||||
@@ -223,6 +273,7 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
|
||||
onDataError = () => {
|
||||
this.data = [];
|
||||
this.annotations = [];
|
||||
this.render();
|
||||
};
|
||||
|
||||
@@ -240,6 +291,11 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
this.render();
|
||||
};
|
||||
|
||||
onEditorRemoveThresholds = () => {
|
||||
this.panel.color.thresholds = [];
|
||||
this.render();
|
||||
};
|
||||
|
||||
onEditorAddThreeLights = () => {
|
||||
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
|
||||
this.panel.color.thresholds.push({color: "yellow", value: 1, tooltip: "warning" });
|
||||
@@ -247,6 +303,19 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
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) => {
|
||||
rendering(scope, elem, attrs, ctrl);
|
||||
};
|
||||
@@ -261,28 +330,27 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
minValue: 0,
|
||||
multipleValues: 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;}
|
||||
|
||||
// collect uniq targets and their indexes in data array
|
||||
cardsData.targetIndex = {};
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let ts = data[i];
|
||||
let target = ts.target;
|
||||
if (cardsData.targetIndex[target] == undefined) {
|
||||
cardsData.targetIndex[target] = []
|
||||
}
|
||||
cardsData.targetIndex[target].push(i);
|
||||
}
|
||||
// Collect uniq timestamps from data and spread over targets and timestamps
|
||||
|
||||
// collect uniq targets and their indices
|
||||
_.map(data, (d, i) => {
|
||||
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i)
|
||||
});
|
||||
|
||||
// TODO add some logic for targets heirarchy
|
||||
cardsData.targets = _.keys(cardsData.targetIndex);
|
||||
cardsData.targets.sort();
|
||||
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.
|
||||
// TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
|
||||
for(let i = 0; i < cardsData.targets.length; i++) {
|
||||
let target = cardsData.targets[i];
|
||||
|
||||
@@ -292,15 +360,19 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
values: [],
|
||||
multipleValues: false,
|
||||
noColorDefined: false,
|
||||
y: target,
|
||||
x: -1,
|
||||
};
|
||||
|
||||
// collect values from all timeseries with target
|
||||
for (let si = 0; si < cardsData.targetIndex[target].length; si++) {
|
||||
let s = data[cardsData.targetIndex[target][si]];
|
||||
if (s.datapoints.length <= j) {
|
||||
continue;
|
||||
}
|
||||
let datapoint = s.datapoints[j];
|
||||
if (card.values.length === 0) {
|
||||
card.x = datapoint[TIME_INDEX];
|
||||
card.y = s.target;
|
||||
}
|
||||
card.values.push(datapoint[VALUE_INDEX]);
|
||||
}
|
||||
@@ -320,9 +392,11 @@ export class StatusHeatmapCtrl extends MetricsPanelCtrl {
|
||||
if (cardsData.minValue > card.minValue)
|
||||
cardsData.minValue = card.minValue;
|
||||
|
||||
if (card.x != -1) {
|
||||
cardsData.cards.push(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cardsData;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,7 @@ export class StatusHeatmapTooltip {
|
||||
add() {
|
||||
this.tooltip = d3.select("body")
|
||||
.append("div")
|
||||
.attr("class", "heatmap-tooltip graph-tooltip grafana-tooltip");
|
||||
.attr("class", "statusmap-tooltip graph-tooltip grafana-tooltip");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@@ -79,7 +79,7 @@ export class StatusHeatmapTooltip {
|
||||
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
|
||||
|
||||
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
|
||||
<div class="status-heatmap-histogram"></div>`;
|
||||
<div class="statusmap-histogram"></div>`;
|
||||
|
||||
if (this.panel.color.mode === 'discrete') {
|
||||
let statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values);
|
||||
@@ -94,7 +94,7 @@ export class StatusHeatmapTooltip {
|
||||
name: <b>${y}</b> <br>
|
||||
${statusesHtml}
|
||||
<ul>
|
||||
${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}; padding: 1px; font-weight: bold; text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF">${v.tooltip}</li>`), "")}
|
||||
${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}" class="discrete-item">${v.tooltip}</li>`), "")}
|
||||
</ul>
|
||||
</div>`;
|
||||
} else {
|
||||
|
||||