38 Commits
Author SHA1 Message Date
Ivan MikheykinandGitHub 012bcac0ef Merge pull request #130 from flant/fix_null_object
fix: proper fallback to string event for renderComplete
2020-08-28 17:59:44 +03:00
Ivan Mikheykin f037ad082e fix: proper fallback to string event for renderComplete 2020-08-28 17:58:43 +03:00
Ivan MikheykinandGitHub 9a2e4a394e Merge pull request #122 from flant/fix_0_3_2
fix: plugin.json fields and relative links in README.md
2020-08-13 14:35:30 +03:00
Ivan Mikheykin 4bd3ecbcf2 fix: plugin.json fields and relative links in README.md 2020-08-13 14:23:25 +03:00
Ivan MikheykinandGitHub 4b7d111cd6 Merge pull request #119 from flant/fix_0_3_1
fix: set version to 0.3.1
2020-07-21 17:51:35 +03:00
Ivan Mikheykin 80725f6cd5 fix: set version to 0.3.1 2020-07-21 17:49:57 +03:00
Ivan MikheykinandGitHub 3e37a0c3cc Merge pull request #118 from flant/fix_for_7_1_0
fix: Grafana 7.1.0 compatibility
2020-07-21 17:43:24 +03:00
Ivan Mikheykin a4d48eb591 fix: Grafana 7.1.0 compatibility
- remove unused injections from module's constructor
- fix trash icon multiplication in options_editor
2020-07-21 17:39:49 +03:00
Ivan MikheykinandGitHub b96d205787 Merge pull request #117 from flant/dependabot/npm_and_yarn/lodash-4.17.19
build(deps): bump lodash from 4.17.15 to 4.17.19
2020-07-21 17:38:08 +03:00
dependabot[bot]andGitHub bd30626721 build(deps): bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-18 23:34:30 +00:00
Ivan MikheykinandGitHub f02831fcbe Merge pull request #115 from flant/fix_before_0_3_0
version 0.3.0
2020-07-14 13:13:44 +03:00
Ivan Mikheykin 7c2f9a9114 0.3.0 2020-07-14 12:41:39 +03:00
Ivan Mikheykin 276d3cce92 fix: dpi for screenshots 2020-07-14 12:31:47 +03:00
Ivan MikheykinandGitHub 46177c71e8 Merge pull request #113 from flant/fix_help_and_docs
help for tooltip and pagination
2020-07-13 23:27:21 +03:00
Ivan Mikheykin c7e7cd33bc fix: add pagination to README 2020-07-13 23:24:18 +03:00
Ivan Mikheykin 6d4c9bb0ef fix: tooltip documentation and help 2020-07-13 23:03:46 +03:00
Ivan MikheykinandGitHub 7f8264a893 Merge pull request #112 from flant/feat_pagination
feat: pagination

See issue #83, pulls: #93 and #112.
2020-07-13 21:35:41 +03:00
Ivan Mikheykin ef4b0e1228 feat: pagination
Continuation of PR #93.

- reworked layout for pagination controls on graph
- use BucketMatrixPager for pagination logic and state
2020-07-13 21:31:14 +03:00
Joaquín Jiménez GarcíaandIvan Mikheykin 99ba3cbdd6 feat: pagination
Continuation of PR #93.
2020-07-13 14:16:37 +03:00
Ivan MikheykinandGitHub 4af775ee1f Merge pull request #110 from flant/fix_deprecated_messages
fix: remove messages 'Using strings as events are deprecated'
2020-07-10 14:07:48 +03:00
Ivan Mikheykin 7817a3928a fix: remove messages 'Using strings as events are deprecated' 2020-07-10 14:02:53 +03:00
Ivan MikheykinandGitHub 080f358e1b Merge pull request #109 from flant/feat_one_tooltip
ref: one codebase for the hover tooltip and the frozen tooltip
2020-07-09 13:28:26 +03:00
Ivan Mikheykin 8c7279561d ref: one codebase for hover tooltip and freezed tooltip
- rename variables for URL template, use templateSrv to render URL links
- fix visibility of tooltip items and url template help popup whe the light theme is used
- rework Tooltip settings pane, organize tooltip editor fields
- rename properties in panel config, implement migration logic for legacy configuration
- close button for the frozen tooltip
- use relative positioning for proper page scrolling
2020-07-09 13:16:09 +03:00
Ivan MikheykinandGitHub 4e281e53d5 Merge pull request #107 from flant/fix_edit_mode_render_for_g7
fix: render buckets in edit mode for Grafana 7.0
2020-07-03 13:25:14 +03:00
Ivan Mikheykin 2479569d71 fix: render buckets in edit mode for Grafana 7.0 2020-07-03 13:19:17 +03:00
Ivan MikheykinandGitHub a911d64a27 Merge pull request #105 from flant/dependabot/npm_and_yarn/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4
2020-07-03 13:11:15 +03:00
dependabot[bot]andGitHub d592dd8ca2 Bump websocket-extensions from 0.1.3 to 0.1.4
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-07 14:16:47 +00:00
Ivan MikheykinandGitHub d7338a7409 Merge pull request #101 from flant/fix_5_4_regression_query_undefined
fix: regression in 5.4 after implementing workaround for 6.7
2020-05-24 15:10:02 +03:00
Ivan Mikheykin 48597eda59 fix: regression in 5.4 after implementing workaround for 6.7 2020-05-24 15:08:23 +03:00
Ivan MikheykinandGitHub 129a37bda6 Merge pull request #99 from flant/fix_opacity_mode_choose_color
fix: opacity color chooser
2020-05-23 14:21:59 +03:00
Ivan Mikheykin f4d2ede57b fix: opacity color chooser 2020-05-23 14:20:21 +03:00
Ivan MikheykinandGitHub 80548d3972 Merge pull request #98 from flant/fix_6_7
fix: make it work in 6.7 and 7.0+
2020-05-23 13:16:28 +03:00
Ivan Mikheykin 6b6219c1a9 fix: make it work in 6.7 and 7.0+ 2020-05-23 13:11:59 +03:00
b33248c664 Fixed coloring values when filter is -1 (#91)
* Fixed coloring values when filter is -1

Co-Authored-By: Ivan Mikheykin <ivan.mikheykin@flant.com>
2020-03-04 20:10:15 +03:00
Ivan MikheykinandGitHub 28d4f51876 Merge pull request #90 from flant/fix_buckets_and_cards
feat: use timestamp to put values in buckets
2020-03-04 17:20:31 +03:00
Ivan Mikheykin 9463913189 feat: use timestamp to put values in buckets
- fix rendering for spreaded values
- fix legend for spectrum and opacity
2020-03-04 17:17:05 +03:00
Ivan MikheykinandGitHub a3f1495680 Merge pull request #86 from JoaquinJimenezGarcia/tooltip_with_urls
Created a new tooltip when click in a value to have different external links
2020-03-04 17:16:40 +03:00
Joaquin Jimenez Garcia 18574c3fa1 Created a new tooltip when click in a value to have different external links. Which links can be created with values from the series. 2020-02-04 16:31:43 +01:00
79 changed files with 6279 additions and 954 deletions
+1
View File
@@ -2,3 +2,4 @@ node_modules
.jshintrc .jshintrc
.idea .idea
.sass-cache .sass-cache
.tscache
+12
View File
@@ -1,5 +1,17 @@
# Changelog # Changelog
## v0.3.1
- Fix to work in Grafana 7.1.0
## v0.3.0
- Tooltip freeze on click
- Show URLs in tooltip
- Pagination controls
- Fix tooltip and null buckets
- Fix display for 6.7+, 7.0+
## v0.2.0 ## v0.2.0
- Migrate code base to typescript - Migrate code base to typescript
+29 -5
View File
@@ -26,9 +26,9 @@
- InfluxDB - InfluxDB
- Mysql - Mysql
* Tested with Grafana: * Tested with Grafana:
- 5.1.3
- 5.4.3 - 5.4.3
- 6.0.0 - 6.3, 6.6, 6.7
- 7.0, 7.1
## Installation ## Installation
@@ -161,7 +161,7 @@ __Multiple values__ checkbox specifies how they should be displayed:
![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/multiple-values-error.png) ![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/multiple-values-error.png)
__Null values__ can be treated as empty buckets or displayed with the color of `0` value. __Display nulls__ can be treated as empty buckets or displayed with the color of `0` value.
![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/null-as-empty.png) ![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/null-as-empty.png)
@@ -172,15 +172,39 @@ __Rounding__ may be used to round edges.
![Min width, spacing, rounding 2](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding-02.png) ![Min width, spacing, rounding 2](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding-02.png)
__Values index__ set to positive number to display only values from specified timeseries.
#### Display #### Display
![Display options](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/options-display.png) ![Display options](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/options-display.png)
__Show legend__ checkbox toggles legend at the bottom of the panel. __Show legend__ checkbox toggles legend at the bottom of the panel.
__Rows 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.
#### Pagination
![Pagination controls](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/pagination-graph.png)
__Enable pagination__ toggles pagination controls on graph.
__Rows per page__ a number of rows to display on graph.
#### Tooltip
![Tooltip in frozen state](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/tooltip-frozen.png)
__Show tooltip__ toggles tooltip display on mouse over buckets. __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. __Freeze on click__ toggles tooltip "freezing" on click. Frozen tooltip can be used to compare data with floating tooltip or to follow URLs.
__Show items__ toggles display of additional items in tooltip.
__Items__ is a list of definitions to display URLs in tooltip.
Each URL has a template, icon, label and formating options: lowercase and date format for variables.
![Tooltip items editor](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/tooltip-editor.png)
## Development ## Development
@@ -191,7 +215,7 @@ the directory containing Statusmap plugin):
docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \ docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \
-p 3000:3000 --name grafana.docker \ -p 3000:3000 --name grafana.docker \
--env=GF_USERS_DEFAULT_THEME=light \ --env=GF_USERS_DEFAULT_THEME=light \
grafana/grafana:5.1.3 grafana/grafana:7.1.3
``` ```
This will expose local plugin from your machine to Grafana container. Now run `grunt` to compile This will expose local plugin from your machine to Grafana container. Now run `grunt` to compile
+12
View File
@@ -1,5 +1,17 @@
# Changelog # Changelog
## v0.3.1
- Fix to work in Grafana 7.1.0
## v0.3.0
- Tooltip freeze on click
- Show URLs in tooltip
- Pagination controls
- Fix tooltip and null buckets
- Fix display for 6.7+, 7.0+
## v0.2.0 ## v0.2.0
- Migrate code base to typescript - Migrate code base to typescript
+29 -5
View File
@@ -26,9 +26,9 @@
- InfluxDB - InfluxDB
- Mysql - Mysql
* Tested with Grafana: * Tested with Grafana:
- 5.1.3
- 5.4.3 - 5.4.3
- 6.0.0 - 6.3, 6.6, 6.7
- 7.0, 7.1
## Installation ## Installation
@@ -161,7 +161,7 @@ __Multiple values__ checkbox specifies how they should be displayed:
![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/multiple-values-error.png) ![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/multiple-values-error.png)
__Null values__ can be treated as empty buckets or displayed with the color of `0` value. __Display nulls__ can be treated as empty buckets or displayed with the color of `0` value.
![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/null-as-empty.png) ![Color mapping](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/null-as-empty.png)
@@ -172,15 +172,39 @@ __Rounding__ may be used to round edges.
![Min width, spacing, rounding 2](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding-02.png) ![Min width, spacing, rounding 2](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/min-width-spacing-rounding-02.png)
__Values index__ set to positive number to display only values from specified timeseries.
#### Display #### Display
![Display options](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/options-display.png) ![Display options](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/options-display.png)
__Show legend__ checkbox toggles legend at the bottom of the panel. __Show legend__ checkbox toggles legend at the bottom of the panel.
__Rows 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.
#### Pagination
![Pagination controls](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/pagination-graph.png)
__Enable pagination__ toggles pagination controls on graph.
__Rows per page__ a number of rows to display on graph.
#### Tooltip
![Tooltip in frozen state](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/tooltip-frozen.png)
__Show tooltip__ toggles tooltip display on mouse over buckets. __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. __Freeze on click__ toggles tooltip "freezing" on click. Frozen tooltip can be used to compare data with floating tooltip or to follow URLs.
__Show items__ toggles display of additional items in tooltip.
__Items__ is a list of definitions to display URLs in tooltip.
Each URL has a template, icon, label and formating options: lowercase and date format for variables.
![Tooltip items editor](https://raw.githubusercontent.com/flant/grafana-statusmap/master/src/img/tooltip-editor.png)
## Development ## Development
@@ -191,7 +215,7 @@ the directory containing Statusmap plugin):
docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \ docker run --rm -it -v $PWD:/var/lib/grafana/plugins/flant-statusmap-panel \
-p 3000:3000 --name grafana.docker \ -p 3000:3000 --name grafana.docker \
--env=GF_USERS_DEFAULT_THEME=light \ --env=GF_USERS_DEFAULT_THEME=light \
grafana/grafana:5.1.3 grafana/grafana:7.1.3
``` ```
This will expose local plugin from your machine to Grafana container. Now run `grunt` to compile This will expose local plugin from your machine to Grafana container. Now run `grunt` to compile
+35 -19
View File
@@ -1,9 +1,9 @@
"use strict"; "use strict";
System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "app/core/core", "app/core/utils/ticks", "app/core/core_module"], function (_export, _context) { System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "app/core/core", "app/core/utils/ticks", "app/core/core_module", "./libs/grafana/events/index"], function (_export, _context) {
"use strict"; "use strict";
var _, $, d3, d3ScaleChromatic, contextSrv, tickStep, coreModule, LEGEND_STEP_WIDTH; var _, $, d3, d3ScaleChromatic, contextSrv, tickStep, coreModule, PanelEvents, LEGEND_STEP_WIDTH;
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) { function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) {
var legendElem = $(elem).find('svg'); var legendElem = $(elem).find('svg');
@@ -21,7 +21,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
legend.selectAll(".status-heatmap-color-legend-rect").data(valuesRange).enter().append("rect") // translate from range space into pixels 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 // and shift all rectangles to the right by 10
.attr("x", function (d) { .attr("x", function (d) {
return d * widthFactor + 10; return (d - rangeFrom) * widthFactor + 10;
}).attr("y", 0) // rectangles are slightly overlaped to prevent gaps }).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) { .attr("width", LEGEND_STEP_WIDTH + 1).attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
return colorScale(d); return colorScale(d);
@@ -53,7 +53,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth); drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
} }
function drawDiscreteColorLegend(elem, colorOptions, discreteHelper) { function drawDiscreteColorLegend(elem, colorOptions, discreteExtraSeries) {
var legendElem = $(elem).find('svg'); var legendElem = $(elem).find('svg');
var legend = d3.select(legendElem.get(0)); var legend = d3.select(legendElem.get(0));
clearLegend(elem); clearLegend(elem);
@@ -65,7 +65,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
var valuesNumber = thresholds.length; // graph width as a fallback var valuesNumber = thresholds.length; // graph width as a fallback
var $heatmap = $(elem).parent().parent().parent().find('.status-heatmap-panel'); var $heatmap = $(elem).parent().parent().parent().find('.statusmap-panel');
var graphWidthAttr = $heatmap.find('svg').attr("width"); var graphWidthAttr = $heatmap.find('svg').attr("width");
var graphWidth = parseInt(graphWidthAttr); // calculate max width of tooltip and use it as width for each item var graphWidth = parseInt(graphWidthAttr); // calculate max width of tooltip and use it as width for each item
@@ -87,7 +87,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
return d * itemWidth; return d * itemWidth;
}).attr("y", 0).attr("width", itemWidth + 1) // Overlap rectangles to prevent gaps }).attr("y", 0).attr("width", itemWidth + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) { .attr("height", legendHeight).attr("stroke-width", 0).attr("fill", function (d) {
return discreteHelper.getDiscreteColor(d); return discreteExtraSeries.getDiscreteColor(d);
}); });
drawDiscreteLegendValues(elem, colorOptions, legendWidth); drawDiscreteLegendValues(elem, colorOptions, legendWidth);
} }
@@ -100,8 +100,8 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
return; return;
} }
var legendValueScale = d3.scaleLinear().domain([0, rangeTo]).range([0, legendWidth]); var legendValueScale = d3.scaleLinear().domain([rangeFrom, rangeTo]).range([0, legendWidth]);
var ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); var ticks = buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue);
var xAxis = d3.axisBottom(legendValueScale).tickValues(ticks).tickSize(2); var xAxis = d3.axisBottom(legendValueScale).tickValues(ticks).tickSize(2);
var colorRect = legendElem.find(":first-child"); var colorRect = legendElem.find(":first-child");
var posY = getSvgElemHeight(legendElem) + 2; var posY = getSvgElemHeight(legendElem) + 2;
@@ -120,8 +120,8 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
} }
var valuesNumber = thresholds.length; var valuesNumber = thresholds.length;
var rangeStep = Math.floor(legendWidth / valuesNumber); var rangeStep = Math.floor(legendWidth / valuesNumber); //let valuesRange = d3.range(0, legendWidth, rangeStep);
var valuesRange = d3.range(0, legendWidth, rangeStep);
var legendValueScale = d3.scaleLinear().domain([0, valuesNumber]).range([0, legendWidth]); var legendValueScale = d3.scaleLinear().domain([0, valuesNumber]).range([0, legendWidth]);
var thresholdValues = []; var thresholdValues = [];
var thresholdTooltips = []; var thresholdTooltips = [];
@@ -133,7 +133,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
var xAxis = d3.axisBottom(legendValueScale).tickValues(d3.range(0, valuesNumber, 1)) //thresholdValues) var xAxis = d3.axisBottom(legendValueScale).tickValues(d3.range(0, valuesNumber, 1)) //thresholdValues)
.tickSize(2).tickFormat(function (t) { .tickSize(2).tickFormat(function (t) {
var i = Math.floor(t); var i = Math.floor(t.valueOf());
var v = thresholdTooltips[i]; var v = thresholdTooltips[i];
if (v != undefined) { if (v != undefined) {
@@ -252,7 +252,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
var ticks = []; var ticks = [];
for (var i = 0; i < ticksNum; i++) { for (var i = 0; i < ticksNum; i++) {
var current = tickStepSize * i; // Add user-defined min and max if it had been set var current = tickStepSize * i + rangeFrom; // Add user-defined min and max if it had been set
if (isValueCloseTo(minValue, current, tickStepSize)) { if (isValueCloseTo(minValue, current, tickStepSize)) {
ticks.push(minValue); ticks.push(minValue);
@@ -268,7 +268,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
ticks.push(maxValue); ticks.push(maxValue);
} }
ticks.push(tickStepSize * i); ticks.push(current);
} }
if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) { if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
@@ -300,6 +300,8 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
tickStep = _appCoreUtilsTicks.tickStep; tickStep = _appCoreUtilsTicks.tickStep;
}, function (_appCoreCore_module) { }, function (_appCoreCore_module) {
coreModule = _appCoreCore_module.default; coreModule = _appCoreCore_module.default;
}, function (_libsGrafanaEventsIndex) {
PanelEvents = _libsGrafanaEventsIndex.PanelEvents;
}], }],
execute: function () { execute: function () {
LEGEND_STEP_WIDTH = 2; LEGEND_STEP_WIDTH = 2;
@@ -315,7 +317,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
var ctrl = scope.ctrl; var ctrl = scope.ctrl;
var panel = scope.ctrl.panel; var panel = scope.ctrl.panel;
render(); render();
ctrl.events.on('render', function () { ctrl.events.on(PanelEvents.render, function () {
render(); render();
}); });
@@ -350,7 +352,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
var ctrl = scope.ctrl; var ctrl = scope.ctrl;
var panel = scope.ctrl.panel; var panel = scope.ctrl.panel;
render(); render();
ctrl.events.on('render', function () { ctrl.events.on(PanelEvents.render, function () {
render(); render();
}); });
@@ -361,12 +363,26 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
return; return;
} }
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) { if (ctrl.bucketMatrix) {
var rangeFrom = ctrl.cardsData.minValue; var rangeFrom = ctrl.bucketMatrix.minValue;
var rangeTo = ctrl.cardsData.maxValue; var rangeTo = ctrl.bucketMatrix.maxValue;
var maxValue = panel.color.max || rangeTo; var maxValue = panel.color.max || rangeTo;
var minValue = panel.color.min || rangeFrom; var minValue = panel.color.min || rangeFrom;
if (ctrl.bucketMatrix.noDatapoints) {
if (!panel.color.max) {
rangeTo = maxValue = 100;
} else {
rangeTo = 100;
}
if (!panel.color.min) {
rangeFrom = minValue = 0;
} else {
rangeFrom = 0;
}
}
if (panel.color.mode === 'spectrum') { if (panel.color.mode === 'spectrum') {
var colorScheme = _.find(ctrl.colorSchemes, { var colorScheme = _.find(ctrl.colorSchemes, {
value: panel.color.colorScheme value: panel.color.colorScheme
@@ -378,7 +394,7 @@ System.register(["lodash", "jquery", "d3", "./libs/d3-scale-chromatic/index", "a
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue); drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
} else if (panel.color.mode === 'discrete') { } else if (panel.color.mode === 'discrete') {
var _colorOptions = panel.color; var _colorOptions = panel.color;
drawDiscreteColorLegend(elem, _colorOptions, ctrl.discreteHelper); drawDiscreteColorLegend(elem, _colorOptions, ctrl.discreteExtraSeries);
} }
} }
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+75 -16
View File
@@ -16,7 +16,7 @@ System.register([], function (_export, _context) {
return { return {
setters: [], setters: [],
execute: function () { execute: function () {
// Helper methods to handle discrete color mode // Extra Series methods to handle discrete color mode
_export("ColorModeDiscrete", ColorModeDiscrete = _export("ColorModeDiscrete", ColorModeDiscrete =
/*#__PURE__*/ /*#__PURE__*/
function () { function () {
@@ -55,6 +55,24 @@ System.register([], function (_export, _context) {
return tooltips; return tooltips;
} }
}, { }, {
key: "convertValueToTooltips",
value: function convertValueToTooltips(values) {
var thresholds = this.panel.color.thresholds;
var tooltips = [];
for (var i = 0; i < thresholds.length; i++) {
//for (let j = 0; j < values.length; j++) {
if (values == thresholds[i].value) {
tooltips.push({
"tooltip": thresholds[i].tooltip ? thresholds[i].tooltip : values,
"color": thresholds[i].color
}); //}
}
}
return tooltips;
}
}, {
key: "getNotMatchedValues", key: "getNotMatchedValues",
value: function getNotMatchedValues(values) { value: function getNotMatchedValues(values) {
var notMatched = []; var notMatched = [];
@@ -92,6 +110,23 @@ System.register([], function (_export, _context) {
} }
return color; return color;
}
}, {
key: "getBucketColorSingle",
value: function getBucketColorSingle(value) {
//let thresholds = this.panel.color.thresholds;
if (value == null) {
// treat as null value
return 'rgba(0,0,0,1)'; //return this.getMatchedThreshold(null).color;
}
var threshold = this.getMatchedThreshold(value);
if (!threshold || !threshold.color || threshold.color == "") {
return 'rgba(0,0,0,1)';
} else {
return threshold.color;
}
} // returns color from first matched thresold in order from 0 to thresholds.length } // returns color from first matched thresold in order from 0 to thresholds.length
}, { }, {
@@ -137,29 +172,53 @@ System.register([], function (_export, _context) {
return 'rgba(0,0,0,1)'; return 'rgba(0,0,0,1)';
} }
}, { }, {
key: "updateCardsValuesHasColorInfo", key: "updateCardsValuesHasColorInfoSingle",
value: function updateCardsValuesHasColorInfo() { value: function updateCardsValuesHasColorInfoSingle() {
if (!this.panelCtrl.cardsData) { var _this = this;
if (!this.panelCtrl.bucketMatrix) {
return; return;
} }
this.panelCtrl.cardsData.noColorDefined = false; this.panelCtrl.bucketMatrix.noColorDefined = false;
var cards = this.panelCtrl.cardsData.cards; this.panelCtrl.bucketMatrix.targets.map(function (target) {
_this.panelCtrl.bucketMatrix.buckets[target].map(function (bucket) {
bucket.noColorDefined = false;
for (var i = 0; i < cards.length; i++) { var threshold = _this.getMatchedThreshold(bucket.value);
cards[i].noColorDefined = false;
var values = cards[i].values;
for (var j = 0; j < values.length; j++) {
var threshold = this.getMatchedThreshold(values[j]);
if (!threshold || !threshold.color || threshold.color == "") { if (!threshold || !threshold.color || threshold.color == "") {
cards[i].noColorDefined = true; bucket.noColorDefined = true;
this.panelCtrl.cardsData.noColorDefined = true; _this.panelCtrl.bucketMatrix.noColorDefined = true;
break;
} }
} });
});
}
}, {
key: "updateCardsValuesHasColorInfo",
value: function updateCardsValuesHasColorInfo() {
var _this2 = this;
if (!this.panelCtrl.bucketMatrix) {
return;
} }
this.panelCtrl.bucketMatrix.noColorDefined = false;
this.panelCtrl.bucketMatrix.targets.map(function (target) {
_this2.panelCtrl.bucketMatrix.buckets[target].map(function (bucket) {
bucket.noColorDefined = false;
for (var j = 0; j < bucket.values.length; j++) {
var threshold = _this2.getMatchedThreshold(bucket.values[j]);
if (!threshold || !threshold.color || threshold.color == "") {
bucket.noColorDefined = true;
_this2.panelCtrl.bucketMatrix.noColorDefined = true;
break;
}
}
});
});
} }
}, { }, {
key: "getMatchedThreshold", key: "getMatchedThreshold",
+1 -1
View File
File diff suppressed because one or more lines are too long
+44 -7
View File
@@ -7,30 +7,43 @@
color: #d8d9da; color: #d8d9da;
padding: 1px; } padding: 1px; }
.status-heatmap-panel { .statusmap-panel {
position: relative; } position: relative; }
.status-heatmap-panel .axis .tick text { .statusmap-panel .axis .tick text {
fill: #d8d9da; fill: #d8d9da;
color: #d8d9da; color: #d8d9da;
font-size: 11px; } font-size: 11px; }
.status-heatmap-panel .axis .tick line { .statusmap-panel .axis .tick line {
opacity: 0.4; opacity: 0.4;
stroke: #8e8e8e; } stroke: #8e8e8e; }
.status-heatmap-panel svg { .statusmap-panel svg {
pointer-events: none; } pointer-events: none; }
.status-heatmap-panel svg rect { .statusmap-panel svg rect {
pointer-events: visiblePainted; } pointer-events: visiblePainted; }
.statusmap-tooltip { .statusmap-tooltip {
position: absolute;
padding: 10px;
font-weight: 200;
border-radius: 5px;
max-width: 800px;
max-height: 600px;
overflow: hidden;
line-height: 14px;
z-index: 9999;
white-space: nowrap; white-space: nowrap;
font-size: 12px; font-size: 12px;
background-color: #141414; background-color: #141414;
color: #d8d9da; } color: #d8d9da; }
.statusmap-tooltip .discrete-item { .statusmap-tooltip .discrete-item {
color: #52545c; color: #ffffff;
padding: 1px; padding: 1px;
font-weight: bold; font-weight: bold;
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF; } text-shadow: 0 0 0.2em #212124, 0 0 0.2em #212124, 0 0 0.2em #212124; }
.statusmap-tooltip-frozen {
position: relative;
z-index: 1; }
.statusmap-histogram rect { .statusmap-histogram rect {
fill: #8e8e8e; } fill: #8e8e8e; }
@@ -44,6 +57,30 @@
fill: rgba(102, 102, 102, 0.4); fill: rgba(102, 102, 102, 0.4);
stroke: rgba(102, 102, 102, 0.8); } stroke: rgba(102, 102, 102, 0.8); }
.width-c-40 {
width: 40rem !important; }
.width-c-50 {
width: 50rem !important; }
.width-c-60 {
width: 60rem !important; }
.width-c-70 {
width: 70rem !important; }
.statusmap-pagination {
margin-top: 5px; }
.statusmap-pagination label {
margin-right: 0px; }
.statusmap-pagination input {
margin-right: 0px; }
.statusmap-pagination .last-in-group {
margin-right: 4px; }
.pagination-buttons {
text-align: right; }
.status-heatmap-legend-wrapper { .status-heatmap-legend-wrapper {
margin: 0 10px; } margin: 0 10px; }
.status-heatmap-legend-wrapper svg { .status-heatmap-legend-wrapper svg {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"version": 3, "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", "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,gBAAiB;EACf,QAAQ,EAAE,QAAQ;EAGhB,iCAAK;IACH,IAAI,ECpBG,OAAO;IDqBd,KAAK,ECrBE,OAAO;IDsBd,SAAS,EEtBE,IAAI;EFyBjB,iCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EC1BM,OAAO;ED+BvB,oBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,yBAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,kBAAmB;EAGjB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,GAAG;EAChB,aAAa,EAAE,GAAG;EAClB,SAAS,EAAE,KAAK;EAChB,UAAU,EAAE,KAAK;EACjB,QAAQ,EAAE,MAAM;EAChB,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,IAAI;EAGb,WAAW,EAAE,MAAM;EACnB,SAAS,EEzDI,IAAI;EF0DjB,gBAAgB,ECvDC,OAAO;EDwDxB,KAAK,EC1DM,OAAO;ED4DlB,iCAAe;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,GAAG;IACZ,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,uDAAuD;;AAIxE,yBAA0B;EACxB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,CAAC;;AAGZ,yBAA0B;EACxB,IAAI,ECzEY,OAAO;;AD6EvB,8BAAK;EACH,MAAM,EAAE,OAAgB;EACxB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAGlC,WAAY;EACV,KAAK,EAAE,gBACT;;AACA,WAAY;EACV,KAAK,EAAE,gBACT;;AACA,WAAY;EACV,KAAK,EAAE,gBACT;;AACA,WAAY;EACV,KAAK,EAAE,gBACT;;AAEA,qBAAsB;EAEpB,UAAU,EAAE,GAAG;EACf,2BAAM;IACJ,YAAY,EAAC,GAAG;EAElB,2BAAM;IACJ,YAAY,EAAE,GAAG;EAEnB,oCAAe;IACb,YAAY,EAAE,GAAG;;AAIrB,mBAAmB;EAAC,UAAU,EAAE,KAAK;;AAErC,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,EC3IG,OAAO;ID4Id,KAAK,EC5IE,OAAO;ID6Id,SAAS,EE7IE,IAAI;EFgJjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,ECjJM,OAAO;EDoJrB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,ECtJM,OAAO",
"sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.dark.scss","../../src/css/_variables.scss"], "sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.dark.scss","../../src/css/_variables.scss"],
"names": [], "names": [],
"file": "statusmap.dark.css" "file": "statusmap.dark.css"
+44 -7
View File
@@ -7,30 +7,43 @@
color: #52545c; color: #52545c;
padding: 1px; } padding: 1px; }
.status-heatmap-panel { .statusmap-panel {
position: relative; } position: relative; }
.status-heatmap-panel .axis .tick text { .statusmap-panel .axis .tick text {
fill: #52545c; fill: #52545c;
color: #52545c; color: #52545c;
font-size: 11px; } font-size: 11px; }
.status-heatmap-panel .axis .tick line { .statusmap-panel .axis .tick line {
opacity: 0.4; opacity: 0.4;
stroke: #767980; } stroke: #767980; }
.status-heatmap-panel svg { .statusmap-panel svg {
pointer-events: none; } pointer-events: none; }
.status-heatmap-panel svg rect { .statusmap-panel svg rect {
pointer-events: visiblePainted; } pointer-events: visiblePainted; }
.statusmap-tooltip { .statusmap-tooltip {
position: absolute;
padding: 10px;
font-weight: 200;
border-radius: 5px;
max-width: 800px;
max-height: 600px;
overflow: hidden;
line-height: 14px;
z-index: 9999;
white-space: nowrap; white-space: nowrap;
font-size: 12px; font-size: 12px;
background-color: #dde4ed; background-color: #dde4ed;
color: #52545c; } color: #52545c; }
.statusmap-tooltip .discrete-item { .statusmap-tooltip .discrete-item {
color: #52545c; color: #ffffff;
padding: 1px; padding: 1px;
font-weight: bold; font-weight: bold;
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF; } text-shadow: 0 0 0.2em #212124, 0 0 0.2em #212124, 0 0 0.2em #212124; }
.statusmap-tooltip-frozen {
position: relative;
z-index: 1; }
.statusmap-histogram rect { .statusmap-histogram rect {
fill: #767980; } fill: #767980; }
@@ -44,6 +57,30 @@
fill: rgba(102, 102, 102, 0.4); fill: rgba(102, 102, 102, 0.4);
stroke: rgba(102, 102, 102, 0.8); } stroke: rgba(102, 102, 102, 0.8); }
.width-c-40 {
width: 40rem !important; }
.width-c-50 {
width: 50rem !important; }
.width-c-60 {
width: 60rem !important; }
.width-c-70 {
width: 70rem !important; }
.statusmap-pagination {
margin-top: 5px; }
.statusmap-pagination label {
margin-right: 0px; }
.statusmap-pagination input {
margin-right: 0px; }
.statusmap-pagination .last-in-group {
margin-right: 4px; }
.pagination-buttons {
text-align: right; }
.status-heatmap-legend-wrapper { .status-heatmap-legend-wrapper {
margin: 0 10px; } margin: 0 10px; }
.status-heatmap-legend-wrapper svg { .status-heatmap-legend-wrapper svg {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"version": 3, "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", "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,gBAAiB;EACf,QAAQ,EAAE,QAAQ;EAGhB,iCAAK;IACH,IAAI,ECpBG,OAAO;IDqBd,KAAK,ECrBE,OAAO;IDsBd,SAAS,EEtBE,IAAI;EFyBjB,iCAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,EC1BM,OAAO;ED+BvB,oBAAI;IACF,cAAc,EAAE,IAAI;IAEpB,yBAAK;MACH,cAAc,EAAE,cAAc;;AAKpC,kBAAmB;EAGjB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,GAAG;EAChB,aAAa,EAAE,GAAG;EAClB,SAAS,EAAE,KAAK;EAChB,UAAU,EAAE,KAAK;EACjB,QAAQ,EAAE,MAAM;EAChB,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,IAAI;EAGb,WAAW,EAAE,MAAM;EACnB,SAAS,EEzDI,IAAI;EF0DjB,gBAAgB,ECvDC,OAAO;EDwDxB,KAAK,EC1DM,OAAO;ED4DlB,iCAAe;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,GAAG;IACZ,WAAW,EAAE,IAAI;IACjB,WAAW,EAAE,uDAAuD;;AAIxE,yBAA0B;EACxB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,CAAC;;AAGZ,yBAA0B;EACxB,IAAI,ECzEY,OAAO;;AD6EvB,8BAAK;EACH,MAAM,EAAE,OAAgB;EACxB,YAAY,EAAE,CAAC;;AAInB,yBAA0B;EACxB,YAAY,EAAE,CAAC;EACf,IAAI,EAAE,wBAAwB;EAC9B,MAAM,EAAE,wBAAwB;;AAGlC,WAAY;EACV,KAAK,EAAE,gBACT;;AACA,WAAY;EACV,KAAK,EAAE,gBACT;;AACA,WAAY;EACV,KAAK,EAAE,gBACT;;AACA,WAAY;EACV,KAAK,EAAE,gBACT;;AAEA,qBAAsB;EAEpB,UAAU,EAAE,GAAG;EACf,2BAAM;IACJ,YAAY,EAAC,GAAG;EAElB,2BAAM;IACJ,YAAY,EAAE,GAAG;EAEnB,oCAAe;IACb,YAAY,EAAE,GAAG;;AAIrB,mBAAmB;EAAC,UAAU,EAAE,KAAK;;AAErC,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,EC3IG,OAAO;ID4Id,KAAK,EC5IE,OAAO;ID6Id,SAAS,EE7IE,IAAI;EFgJjB,+CAAK;IACH,OAAO,EAAE,GAAG;IACZ,MAAM,ECjJM,OAAO;EDoJrB,kDAAQ;IACN,OAAO,EAAE,GAAG;IACZ,MAAM,ECtJM,OAAO",
"sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.light.scss","../../src/css/_variables.scss"], "sources": ["../../src/css/_statusmap.scss","../../src/css/_variables.light.scss","../../src/css/_variables.scss"],
"names": [], "names": [],
"file": "statusmap.light.css" "file": "statusmap.light.css"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+11
View File
@@ -0,0 +1,11 @@
"use strict";
System.register([], function (_export, _context) {
"use strict";
return {
setters: [],
execute: function () {}
};
});
//# sourceMappingURL=appEvents.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","sourcesContent":[],"file":"appEvents.js"}
+29
View File
@@ -0,0 +1,29 @@
"use strict";
System.register([], function (_export, _context) {
"use strict";
var graphHover, graphHoverClear;
function fallbackToStringEvents() {
_export("graphHover", graphHover = 'graph-hover');
_export("graphHoverClear", graphHoverClear = 'graph-hover-clear');
}
_export("fallbackToStringEvents", fallbackToStringEvents);
return {
setters: [],
execute: function () {
_export("graphHover", graphHover = {
name: 'graph-hover'
});
_export("graphHoverClear", graphHoverClear = {
name: 'graph-hover-clear'
});
}
};
});
//# sourceMappingURL=events.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/libs/grafana/events/events.ts"],"names":["fallbackToStringEvents","graphHover","graphHoverClear","name"],"mappings":";;;;;;;AAYO,WAASA,sBAAT,GAAkC;AACrC,0BAAAC,UAAU,GAAG,aAAb;;AACA,+BAAAC,eAAe,GAAG,mBAAlB;AACH;;oCAHeF,sB;;;;;4BAHLC,U,GAAkD;AAACE,QAAAA,IAAI,EAAE;AAAP,O;;iCAClDD,e,GAAyC;AAACC,QAAAA,IAAI,EAAE;AAAP,O","sourcesContent":["import {AppEvent} from './appEvents';\n\nexport interface GraphHoverPayload {\n pos: any;\n panel: {\n id: number;\n };\n}\n\nexport var graphHover:(AppEvent<GraphHoverPayload>|string) = {name: 'graph-hover'};\nexport var graphHoverClear:(AppEvent<any>|string) = {name: 'graph-hover-clear'};\n\nexport function fallbackToStringEvents() {\n graphHover = 'graph-hover';\n graphHoverClear = 'graph-hover-clear';\n}\n"],"file":"events.js"}
+20
View File
@@ -0,0 +1,20 @@
"use strict";
System.register(["./events", "./panelEvents"], function (_export, _context) {
"use strict";
var CoreEvents, PanelEvents;
return {
setters: [function (_events) {
CoreEvents = _events;
}, function (_panelEvents) {
PanelEvents = _panelEvents;
}],
execute: function () {
_export("CoreEvents", CoreEvents);
_export("PanelEvents", PanelEvents);
}
};
});
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/libs/grafana/events/index.ts"],"names":["CoreEvents","PanelEvents"],"mappings":";;;;;;;;AAAYA,MAAAA,U;;AACAC,MAAAA,W;;;4BACHD,U;;6BAAYC,W","sourcesContent":["import * as CoreEvents from './events';\nimport * as PanelEvents from './panelEvents';\nexport { CoreEvents, PanelEvents }"],"file":"index.js"}
+53
View File
@@ -0,0 +1,53 @@
"use strict";
System.register([], function (_export, _context) {
"use strict";
var refresh, render, dataError, dataReceived, dataSnapshotLoad, editModeInitialized;
function fallbackToStringEvents() {
_export("refresh", refresh = 'refresh');
_export("render", render = 'render');
_export("dataError", dataError = 'data-error');
_export("dataReceived", dataReceived = 'data-received');
_export("dataSnapshotLoad", dataSnapshotLoad = 'data-snapshot-load');
_export("editModeInitialized", editModeInitialized = 'init-edit-mode');
}
_export("fallbackToStringEvents", fallbackToStringEvents);
return {
setters: [],
execute: function () {
_export("refresh", refresh = {
name: 'refresh'
});
_export("render", render = {
name: 'render'
});
_export("dataError", dataError = {
name: 'data-error'
});
_export("dataReceived", dataReceived = {
name: 'data-received'
});
_export("dataSnapshotLoad", dataSnapshotLoad = {
name: 'data-snapshot-load'
});
_export("editModeInitialized", editModeInitialized = {
name: 'init-edit-mode'
});
}
};
});
//# sourceMappingURL=panelEvents.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/libs/grafana/events/panelEvents.ts"],"names":["fallbackToStringEvents","refresh","render","dataError","dataReceived","dataSnapshotLoad","editModeInitialized","name"],"mappings":";;;;;;;AAqBO,WAASA,sBAAT,GAAkC;AACrC,uBAAAC,OAAO,GAAG,SAAV;;AACA,sBAAAC,MAAM,GAAG,QAAT;;AACA,yBAAAC,SAAS,GAAG,YAAZ;;AACA,4BAAAC,YAAY,GAAG,eAAf;;AACA,gCAAAC,gBAAgB,GAAG,oBAAnB;;AACA,mCAAAC,mBAAmB,GAAG,gBAAtB;AACH;;oCAPeN,sB;;;;;yBAPLC,O,GAAuC;AAACM,QAAAA,IAAI,EAAE;AAAP,O;;wBACvCL,M,GAAgC;AAACK,QAAAA,IAAI,EAAE;AAAP,O;;2BAChCJ,S,GAA8C;AAACI,QAAAA,IAAI,EAAE;AAAP,O;;8BAC9CH,Y,GAAwC;AAACG,QAAAA,IAAI,EAAE;AAAP,O;;kCACxCF,gB,GAA4C;AAACE,QAAAA,IAAI,EAAE;AAAP,O;;qCAC5CD,mB,GAAmD;AAACC,QAAAA,IAAI,EAAE;AAAP,O","sourcesContent":["import { AppEvent } from './appEvents';\n\nexport interface DataQueryError {\n data?: {\n message?: string;\n error?: string;\n };\n message?: string;\n status?: string;\n statusText?: string;\n refId?: string;\n cancelled?: boolean;\n}\n\nexport var refresh:(AppEvent<undefined>|string) = {name: 'refresh'};\nexport var render:(AppEvent<any>|string) = {name: 'render'};\nexport var dataError:(AppEvent<DataQueryError>|string) = {name: 'data-error'};\nexport var dataReceived:(AppEvent<any[]>|string) = {name: 'data-received'};\nexport var dataSnapshotLoad:(AppEvent<any[]>|string) = {name: 'data-snapshot-load'};\nexport var editModeInitialized:(AppEvent<undefined>|string) = {name: 'init-edit-mode'};\n\nexport function fallbackToStringEvents() {\n refresh = 'refresh';\n render = 'render';\n dataError = 'data-error';\n dataReceived = 'data-received';\n dataSnapshotLoad = 'data-snapshot-load';\n editModeInitialized = 'init-edit-mode';\n}\n"],"file":"panelEvents.js"}
+8
View File
@@ -0,0 +1,8 @@
1. @grafana/data is not supported in grafana 5.x and 6.2. The workaround is
the module `./libs/grafana/events` that include copy-pasted events
from @grafana/data and app/core from grafana sources.
2. Newly Grafana versions flood Console with
"Usage strings as events is deprecated ..." messages.
This module has a function to detect if Grafanas Emitter support AppEvent style event ids.
+38
View File
@@ -0,0 +1,38 @@
"use strict";
System.register([], function (_export, _context) {
"use strict";
// Old Grafana releases use strings as event ids and
// new event ids in form of object {name: "event-id"} are not
// supported properly: first defined listener will receive
// all emitted events despite of the value in 'name' field.
//
// This method detects this behaviour and return true
// only for new Grafana versions.
function hasAppEventCompatibleEmitter(emitter) {
var receiveEvents = 0;
var eventId = {
name: "non-existed-event-id"
};
var eventId2 = {
name: "non-existed-event-id-2"
};
emitter.on(eventId, function () {
receiveEvents++;
});
emitter.emit(eventId);
emitter.emit(eventId2);
emitter.removeAllListeners(eventId); // New Grafana versions should receive one event.
return receiveEvents == 1;
}
_export("hasAppEventCompatibleEmitter", hasAppEventCompatibleEmitter);
return {
setters: [],
execute: function () {}
};
});
//# sourceMappingURL=funcs.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/libs/polygrafill/funcs.ts"],"names":["hasAppEventCompatibleEmitter","emitter","receiveEvents","eventId","name","eventId2","on","emit","removeAllListeners"],"mappings":";;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,WAASA,4BAAT,CAAsCC,OAAtC,EAAgE;AACnE,QAAIC,aAAa,GAAG,CAApB;AACA,QAAIC,OAAY,GAAG;AAACC,MAAAA,IAAI,EAAE;AAAP,KAAnB;AACA,QAAIC,QAAa,GAAG;AAACD,MAAAA,IAAI,EAAE;AAAP,KAApB;AACAH,IAAAA,OAAO,CAACK,EAAR,CAAWH,OAAX,EAAoB,YAAU;AAC1BD,MAAAA,aAAa;AAChB,KAFD;AAGAD,IAAAA,OAAO,CAACM,IAAR,CAAaJ,OAAb;AACAF,IAAAA,OAAO,CAACM,IAAR,CAAaF,QAAb;AACAJ,IAAAA,OAAO,CAACO,kBAAR,CAA2BL,OAA3B,EATmE,CAWnE;;AACA,WAAOD,aAAa,IAAI,CAAxB;AACH;;0CAbeF,4B","sourcesContent":["import { Emitter } from 'app/core/utils/emitter';\n\n// Old Grafana releases use strings as event ids and\n// new event ids in form of object {name: \"event-id\"} are not\n// supported properly: first defined listener will receive\n// all emitted events despite of the value in 'name' field.\n//\n// This method detects this behaviour and return true\n// only for new Grafana versions.\nexport function hasAppEventCompatibleEmitter(emitter: Emitter):boolean {\n let receiveEvents = 0;\n let eventId: any = {name: \"non-existed-event-id\"};\n let eventId2: any = {name: \"non-existed-event-id-2\"};\n emitter.on(eventId, function(){\n receiveEvents++;\n });\n emitter.emit(eventId);\n emitter.emit(eventId2);\n emitter.removeAllListeners(eventId);\n\n // New Grafana versions should receive one event.\n return receiveEvents == 1;\n}\n"],"file":"funcs.js"}
+16
View File
@@ -0,0 +1,16 @@
"use strict";
System.register(["./funcs"], function (_export, _context) {
"use strict";
var Polygrafill;
return {
setters: [function (_funcs) {
Polygrafill = _funcs;
}],
execute: function () {
_export("Polygrafill", Polygrafill);
}
};
});
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/libs/polygrafill/index.ts"],"names":["Polygrafill"],"mappings":";;;;;;;;AAAYA,MAAAA,W;;;6BACHA,W","sourcesContent":["import * as Polygrafill from './funcs';\nexport { Polygrafill };"],"file":"index.js"}
+26 -4
View File
@@ -1,15 +1,37 @@
<div class="status-heatmap-wrapper"> <div class="status-heatmap-wrapper">
<div class="status-heatmap-canvas-wrapper"> <div class="status-heatmap-canvas-wrapper">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined || ctrl.noDatapoints">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined">
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span> <span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span> <span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
<span class="small" ng-if="ctrl.noDatapoints" bs-tooltip="'{{ctrl.dataWarnings.noDatapoints.tip}}'">{{ctrl.dataWarnings.noDatapoints.title}}</span>
</div> </div>
<div class="statusmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
<div class="status-heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
</div> </div>
<div class="status-heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show"> <div class="status-heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show">
<status-heatmap-legend></status-heatmap-legend> <status-heatmap-legend></status-heatmap-legend>
</div> </div>
<div class="clearfix"></div>
<div class="statusmap-pagination gf-form" ng-if="ctrl.panel.usingPagination">
<div class="gf-form">
<button class="btn btn-inverse" ng-disabled="!pager.hasPrev()" ng-click="ctrl.onPrevPage()">
<i class="fa fa-arrow-left"></i>&nbsp;Previous
</button>
<label class="gf-form-label">{{pager.currentPage+1}}/{{pager.pages()}}</label>
<button class="btn btn-inverse last-in-group" ng-disabled="!pager.hasNext()" ng-click="ctrl.onNextPage()">
Next&nbsp;<i class="fa fa-arrow-right"></i>
</button>
</div>
<div class="gf-form">
<label class="gf-form-label">Rows per page</label>
<input type="number" class="gf-form-input width-3 last-in-group" placeholder="12" data-placement="top"
bs-tooltip="'Number of rows to show per page.'" ng-model="ctrl.pageSizeViewer" ng-change="ctrl.onChangePageSize()"
ng-model-onblur>
</div>
<label class="gf-form-label">Show {{pager.pageStartRow()}} to {{pager.pageEndRow()}} of {{pager.totalRows()}} rows</label>
</div>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
+419 -236
View File
@@ -1,9 +1,9 @@
"use strict"; "use strict";
System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/sdk", "./statusmap_data", "./rendering", "./options_editor", "./color_mode_discrete"], function (_export, _context) { System.register(["lodash", "./color_legend", "./options_editor", "./tooltip_editor", "./panel_config_migration", "app/core/utils/kbn", "app/plugins/sdk", "./libs/grafana/events/index", "./statusmap_data", "./rendering", "./libs/polygrafill/index", "./color_mode_discrete"], function (_export, _context) {
"use strict"; "use strict";
var _, kbn, loadPluginCss, MetricsPanelCtrl, Card, rendering, statusHeatmapOptionsEditor, ColorModeDiscrete, CANVAS, SVG, VALUE_INDEX, TIME_INDEX, renderer, colorSchemes, colorModes, opacityScales, StatusHeatmapCtrl; var _, optionsEditorCtrl, tooltipEditorCtrl, migratePanelConfig, kbn, loadPluginCss, MetricsPanelCtrl, CoreEvents, PanelEvents, Bucket, BucketMatrix, BucketMatrixPager, rendering, Polygrafill, ColorModeDiscrete, VALUE_INDEX, TIME_INDEX, colorSchemes, colorModes, opacityScales, renderComplete, StatusHeatmapCtrl;
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
@@ -32,26 +32,34 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
return { return {
setters: [function (_lodash) { setters: [function (_lodash) {
_ = _lodash.default; _ = _lodash.default;
}, function (_color_legend) {}, function (_appCoreUtilsKbn) { }, function (_color_legend) {}, function (_options_editor) {
optionsEditorCtrl = _options_editor.optionsEditorCtrl;
}, function (_tooltip_editor) {
tooltipEditorCtrl = _tooltip_editor.tooltipEditorCtrl;
}, function (_panel_config_migration) {
migratePanelConfig = _panel_config_migration.migratePanelConfig;
}, function (_appCoreUtilsKbn) {
kbn = _appCoreUtilsKbn.default; kbn = _appCoreUtilsKbn.default;
}, function (_appPluginsSdk) { }, function (_appPluginsSdk) {
loadPluginCss = _appPluginsSdk.loadPluginCss; loadPluginCss = _appPluginsSdk.loadPluginCss;
MetricsPanelCtrl = _appPluginsSdk.MetricsPanelCtrl; MetricsPanelCtrl = _appPluginsSdk.MetricsPanelCtrl;
}, function (_libsGrafanaEventsIndex) {
CoreEvents = _libsGrafanaEventsIndex.CoreEvents;
PanelEvents = _libsGrafanaEventsIndex.PanelEvents;
}, function (_statusmap_data) { }, function (_statusmap_data) {
Card = _statusmap_data.Card; Bucket = _statusmap_data.Bucket;
BucketMatrix = _statusmap_data.BucketMatrix;
BucketMatrixPager = _statusmap_data.BucketMatrixPager;
}, function (_rendering) { }, function (_rendering) {
rendering = _rendering.default; rendering = _rendering.default;
}, function (_options_editor) { }, function (_libsPolygrafillIndex) {
statusHeatmapOptionsEditor = _options_editor.statusHeatmapOptionsEditor; Polygrafill = _libsPolygrafillIndex.Polygrafill;
}, function (_color_mode_discrete) { }, function (_color_mode_discrete) {
ColorModeDiscrete = _color_mode_discrete.ColorModeDiscrete; ColorModeDiscrete = _color_mode_discrete.ColorModeDiscrete;
}], }],
execute: function () { execute: function () {
CANVAS = 'CANVAS';
SVG = 'SVG';
VALUE_INDEX = 0; VALUE_INDEX = 0;
TIME_INDEX = 1; TIME_INDEX = 1;
renderer = CANVAS;
colorSchemes = [// Diverging colorSchemes = [// Diverging
{ {
name: 'Spectral', name: 'Spectral',
@@ -147,6 +155,10 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css' light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
}); });
_export("renderComplete", renderComplete = {
name: 'statusmap-render-complete'
});
_export("PanelCtrl", _export("StatusHeatmapCtrl", StatusHeatmapCtrl = _export("PanelCtrl", _export("StatusHeatmapCtrl", StatusHeatmapCtrl =
/*#__PURE__*/ /*#__PURE__*/
function (_MetricsPanelCtrl) { function (_MetricsPanelCtrl) {
@@ -154,6 +166,8 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
_inherits(StatusHeatmapCtrl, _MetricsPanelCtrl); _inherits(StatusHeatmapCtrl, _MetricsPanelCtrl);
// TODO remove this transient variable: use ng-model-options="{ getterSetter: true }"
/** @ngInject */ /** @ngInject */
function StatusHeatmapCtrl($scope, $injector, annotationsSrv) { function StatusHeatmapCtrl($scope, $injector, annotationsSrv) {
var _this; var _this;
@@ -163,6 +177,16 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
_this = _possibleConstructorReturn(this, _getPrototypeOf(StatusHeatmapCtrl).call(this, $scope, $injector)); _this = _possibleConstructorReturn(this, _getPrototypeOf(StatusHeatmapCtrl).call(this, $scope, $injector));
_this.annotationsSrv = annotationsSrv; _this.annotationsSrv = annotationsSrv;
_defineProperty(_assertThisInitialized(_this), "data", void 0);
_defineProperty(_assertThisInitialized(_this), "bucketMatrix", void 0);
_defineProperty(_assertThisInitialized(_this), "bucketMatrixPager", void 0);
_defineProperty(_assertThisInitialized(_this), "graph", void 0);
_defineProperty(_assertThisInitialized(_this), "discreteHelper", void 0);
_defineProperty(_assertThisInitialized(_this), "opacityScales", []); _defineProperty(_assertThisInitialized(_this), "opacityScales", []);
_defineProperty(_assertThisInitialized(_this), "colorModes", []); _defineProperty(_assertThisInitialized(_this), "colorModes", []);
@@ -171,24 +195,22 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
_defineProperty(_assertThisInitialized(_this), "unitFormats", void 0); _defineProperty(_assertThisInitialized(_this), "unitFormats", void 0);
_defineProperty(_assertThisInitialized(_this), "data", void 0); _defineProperty(_assertThisInitialized(_this), "dataWarnings", {});
_defineProperty(_assertThisInitialized(_this), "cardsData", void 0);
_defineProperty(_assertThisInitialized(_this), "graph", void 0);
_defineProperty(_assertThisInitialized(_this), "multipleValues", void 0); _defineProperty(_assertThisInitialized(_this), "multipleValues", void 0);
_defineProperty(_assertThisInitialized(_this), "noColorDefined", void 0); _defineProperty(_assertThisInitialized(_this), "noColorDefined", void 0);
_defineProperty(_assertThisInitialized(_this), "discreteHelper", void 0); _defineProperty(_assertThisInitialized(_this), "noDatapoints", void 0);
_defineProperty(_assertThisInitialized(_this), "dataWarnings", void 0); _defineProperty(_assertThisInitialized(_this), "discreteExtraSeries", void 0);
_defineProperty(_assertThisInitialized(_this), "annotations", []); _defineProperty(_assertThisInitialized(_this), "annotations", []);
_defineProperty(_assertThisInitialized(_this), "annotationsPromise", void 0); _defineProperty(_assertThisInitialized(_this), "annotationsPromise", void 0);
_defineProperty(_assertThisInitialized(_this), "pageSizeViewer", 15);
_defineProperty(_assertThisInitialized(_this), "panelDefaults", { _defineProperty(_assertThisInitialized(_this), "panelDefaults", {
// datasource name, null = default datasource // datasource name, null = default datasource
datasource: null, datasource: null,
@@ -212,35 +234,56 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
cardRound: null cardRound: null
}, },
xAxis: { xAxis: {
show: true, show: true
showWeekends: true,
minBucketWidthToShowWeekends: 4,
showCrosshair: true,
labelFormat: '%a %m/%d'
}, },
yAxis: { yAxis: {
show: true, show: true,
showCrosshair: false minWidth: -1,
maxWidth: -1
}, },
tooltip: { tooltip: {
show: true show: true,
freezeOnClick: true,
showItems: false,
items: [] // see tooltip_editor.ts
}, },
legend: { legend: {
show: true show: true
}, },
data: {
unitFormat: 'short',
decimals: null
},
// how null points should be handled // how null points should be handled
nullPointMode: 'as empty', nullPointMode: 'as empty',
yAxisSort: 'metrics', yAxisSort: 'metrics',
highlightCards: true, highlightCards: true,
useMax: true useMax: true,
seriesFilterIndex: -1,
// Pagination options
usingPagination: false,
pageSize: 15
}); });
if (!Polygrafill.hasAppEventCompatibleEmitter(_this.events)) {
CoreEvents.fallbackToStringEvents();
PanelEvents.fallbackToStringEvents();
_export("renderComplete", renderComplete = 'statusmap-render-complete');
}
migratePanelConfig(_this.panel);
_.defaultsDeep(_this.panel, _this.panelDefaults); _.defaultsDeep(_this.panel, _this.panelDefaults);
_this.bucketMatrix = new BucketMatrix(); // Create pager for bucketMatrix and restore page size.
_this.bucketMatrixPager = new BucketMatrixPager();
_this.bucketMatrixPager.setEnable(_this.panel.usingPagination);
_this.bucketMatrixPager.setDefaultPageSize(_this.panel.pageSize);
_this.bucketMatrixPager.setPageSize(_this.panel.pageSize);
$scope.pager = _this.bucketMatrixPager;
_this.opacityScales = opacityScales; _this.opacityScales = opacityScales;
_this.colorModes = colorModes; _this.colorModes = colorModes;
_this.colorSchemes = colorSchemes; // default graph width for discrete card width calculation _this.colorSchemes = colorSchemes; // default graph width for discrete card width calculation
@@ -250,34 +293,40 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
}; };
_this.multipleValues = false; _this.multipleValues = false;
_this.noColorDefined = false; _this.noColorDefined = false;
_this.discreteHelper = new ColorModeDiscrete($scope); _this.discreteExtraSeries = new ColorModeDiscrete($scope);
_this.dataWarnings = { _this.dataWarnings = {
"noColorDefined": { noColorDefined: {
title: 'Data has value with undefined color', title: 'Data has value with undefined color',
tip: 'Check metric values, color values or define a new color' tip: 'Check metric values, color values or define a new color'
}, },
"multipleValues": { multipleValues: {
title: 'Data has multiple values for one target', title: 'Data has multiple values for one target',
tip: 'Change targets definitions or set "use max value"' tip: 'Change targets definitions or set "use max value"'
},
noDatapoints: {
title: 'No data points',
tip: 'No datapoints returned from data query'
} }
}; };
_this.annotations = []; _this.annotations = [];
_this.annotationsSrv = annotationsSrv;
_this.events.on('render', _this.onRender.bind(_assertThisInitialized(_this))); _this.events.on(PanelEvents.render, _this.onRender.bind(_assertThisInitialized(_this)));
_this.events.on('data-received', _this.onDataReceived.bind(_assertThisInitialized(_this))); _this.events.on(PanelEvents.dataReceived, _this.onDataReceived.bind(_assertThisInitialized(_this)));
_this.events.on('data-error', _this.onDataError.bind(_assertThisInitialized(_this))); _this.events.on(PanelEvents.dataError, _this.onDataError.bind(_assertThisInitialized(_this)));
_this.events.on('data-snapshot-load', _this.onDataReceived.bind(_assertThisInitialized(_this))); _this.events.on(PanelEvents.dataSnapshotLoad, _this.onDataReceived.bind(_assertThisInitialized(_this)));
_this.events.on('init-edit-mode', _this.onInitEditMode.bind(_assertThisInitialized(_this))); _this.events.on(PanelEvents.editModeInitialized, _this.onInitEditMode.bind(_assertThisInitialized(_this)));
_this.events.on('refresh', _this.postRefresh.bind(_assertThisInitialized(_this))); // custom event from rendering.js _this.events.on(PanelEvents.refresh, _this.postRefresh.bind(_assertThisInitialized(_this))); // custom event from rendering.js
_this.events.on('render-complete', _this.onRenderComplete.bind(_assertThisInitialized(_this))); _this.events.on(renderComplete, _this.onRenderComplete.bind(_assertThisInitialized(_this)));
_this.onCardColorChange = _this.onCardColorChange.bind(_assertThisInitialized(_this));
return _this; return _this;
} }
@@ -287,18 +336,73 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
this.graph.chartWidth = data.chartWidth; this.graph.chartWidth = data.chartWidth;
this.renderingCompleted(); this.renderingCompleted();
} }
}, {
key: "changeDefaultPaginationSize",
value: function changeDefaultPaginationSize(defaultPageSize) {
this.bucketMatrixPager.setDefaultPageSize(defaultPageSize);
this.bucketMatrixPager.setPageSize(defaultPageSize);
this.pageSizeViewer = defaultPageSize;
this.render();
this.refresh();
}
}, {
key: "onChangePageSize",
value: function onChangePageSize() {
if (this.pageSizeViewer <= 0) {
this.pageSizeViewer = this.bucketMatrixPager.defaultPageSize;
}
this.bucketMatrixPager.setPageSize(this.pageSizeViewer);
this.bucketMatrixPager.setCurrent(0);
this.render();
this.refresh();
}
}, {
key: "onPrevPage",
value: function onPrevPage() {
this.bucketMatrixPager.switchToPrev();
this.render();
}
}, {
key: "onNextPage",
value: function onNextPage() {
this.bucketMatrixPager.switchToNext();
this.render();
} // getChartWidth returns an approximation of chart canvas width or
// a saved value calculated during a render.
}, { }, {
key: "getChartWidth", key: "getChartWidth",
value: function getChartWidth() { value: function getChartWidth() {
if (this.graph.chartWidth > 0) {
return this.graph.chartWidth;
}
var wndWidth = $(window).width(); // gripPos.w is a width in grid's measurements. Grid size in Grafana is 24. var wndWidth = $(window).width(); // gripPos.w is a width in grid's measurements. Grid size in Grafana is 24.
var panelWidthFactor = this.panel.gridPos.w / 24; var panelWidthFactor = this.panel.gridPos.w / 24;
var panelWidth = Math.ceil(wndWidth * panelWidthFactor); // approximate chartWidth because y axis ticks not rendered yet on first data receive. var panelWidth = Math.ceil(wndWidth * panelWidthFactor); // approximate width of the chart draw canvas:
// - y axis ticks are not rendered yet on first data receive,
// so choose 200 as a decent value for y legend width
// - chartWidth can not be lower than the half of the panel width.
var chartWidth = _.max([panelWidth - 200, panelWidth / 2]); var chartWidth = _.max([panelWidth - 200, panelWidth / 2]);
return chartWidth; return chartWidth;
} // override calculateInterval for discrete color mode } // Quick workaround for 6.7 and 7.0+. There is no call to
// calculateInterval in updateTimeRange in those versions.
// TODO ts type has no argument for this method.
}, {
key: "updateTimeRange",
value: function updateTimeRange(datasource) {
var ret = _get(_getPrototypeOf(StatusHeatmapCtrl.prototype), "updateTimeRange", this).call(this, datasource);
this.calculateInterval();
return ret;
} // calculateInterval is called on 'refresh' to calculate an interval
// for datasource.
// It is override of calculateInterval from MetricsPanelCtrl.
}, { }, {
key: "calculateInterval", key: "calculateInterval",
@@ -310,7 +414,7 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
var intervalMs; var intervalMs;
var rangeMs = this.range.to.valueOf() - this.range.from.valueOf(); // this is minimal interval! kbn.round_interval will lower it. var rangeMs = this.range.to.valueOf() - this.range.from.valueOf(); // this is minimal interval! kbn.round_interval will lower it.
intervalMs = this.discreteHelper.roundIntervalCeil(rangeMs / maxCardsCount); // Calculate low limit of interval intervalMs = this.discreteExtraSeries.roundIntervalCeil(rangeMs / maxCardsCount); // Calculate low limit of interval
var lowLimitMs = 1; // 1 millisecond default low limit var lowLimitMs = 1; // 1 millisecond default low limit
@@ -336,7 +440,9 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
var interval = kbn.secondsToHms(intervalMs / 1000); var interval = kbn.secondsToHms(intervalMs / 1000);
this.intervalMs = intervalMs; this.intervalMs = intervalMs;
this.interval = interval; this.interval = interval; // Get final buckets count after interval is adjusted
// TODO is it needed?
//this.xBucketsCount = Math.floor(rangeMs / intervalMs);
} }
}, { }, {
key: "issueQueries", key: "issueQueries",
@@ -354,7 +460,7 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
* (but not wait for completion). This resolves * (but not wait for completion). This resolves
* issue 11806. * issue 11806.
*/ */
// 5.x before 5.4 doesn't have datasourcePromises. // 5.x before 5.4 doesn't have datasourcePromises.
if ("undefined" !== typeof this.annotationsSrv.datasourcePromises) { if ("undefined" !== typeof this.annotationsSrv.datasourcePromises) {
return this.annotationsSrv.datasourcePromises.then(function (r) { return this.annotationsSrv.datasourcePromises.then(function (r) {
@@ -371,7 +477,7 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
key: "issueQueriesWithInterval", key: "issueQueriesWithInterval",
value: function issueQueriesWithInterval(datasource, interval) { value: function issueQueriesWithInterval(datasource, interval) {
var origInterval = this.panel.interval; var origInterval = this.panel.interval;
this.panel.interval = this.interval; this.panel.interval = interval;
var res = _get(_getPrototypeOf(StatusHeatmapCtrl.prototype), "issueQueries", this).call(this, datasource); var res = _get(_getPrototypeOf(StatusHeatmapCtrl.prototype), "issueQueries", this).call(this, datasource);
@@ -383,58 +489,89 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
value: function onDataReceived(dataList) { value: function onDataReceived(dataList) {
var _this3 = this; var _this3 = this;
this.data = dataList; this.data = dataList; // Quick workaround for 7.0+. There is no call to
this.cardsData = this.convertToCards(this.data); // calculateInterval when enter Edit mode.
console.log("OnDataReceived");
this.annotationsPromise.then(function (result) {
_this3.loading = false; //this.alertState = result.alertState;
if (result.annotations && result.annotations.length > 0) { if (!this.intervalMs) {
_this3.annotations = result.annotations; this.calculateInterval();
} else { }
var newBucketMatrix = this.convertDataToBuckets(dataList, this.range.from.valueOf(), this.range.to.valueOf(), this.intervalMs, true);
this.bucketMatrix = newBucketMatrix;
this.bucketMatrixPager.bucketMatrix = newBucketMatrix;
if (newBucketMatrix.targets.length !== this.bucketMatrix.targets.length) {
this.bucketMatrixPager.setCurrent(0);
}
this.noDatapoints = this.bucketMatrix.noDatapoints;
if (this.annotationsPromise) {
this.annotationsPromise.then(function (result) {
_this3.loading = false; //this.alertState = result.alertState;
if (result.annotations && result.annotations.length > 0) {
_this3.annotations = result.annotations;
} else {
_this3.annotations = [];
}
_this3.render();
}, function () {
_this3.loading = false;
_this3.annotations = []; _this3.annotations = [];
}
console.log("annotationsPromise result " + _this3.annotations.length + " annotations"); _this3.render();
});
_this3.render(); } else {
}, function () { this.loading = false;
_this3.loading = false; this.annotations = [];
_this3.annotations = []; this.render();
console.log("annotationsPromise onrejected"); }
_this3.render();
}); //this.render();
} }
}, { }, {
key: "onInitEditMode", key: "onInitEditMode",
value: function onInitEditMode() { value: function onInitEditMode() {
this.addEditorTab('Options', statusHeatmapOptionsEditor, 2); this.addEditorTab('Options', optionsEditorCtrl, 2);
this.addEditorTab('Tooltip', tooltipEditorCtrl, 3);
this.unitFormats = kbn.getUnitFormats(); this.unitFormats = kbn.getUnitFormats();
} } // onRender will be called before StatusmapRenderer.onRender.
// Decide if warning should be displayed over cards.
}, { }, {
key: "onRender", key: "onRender",
value: function onRender() { value: function onRender() {
if (!this.range || !this.data) { if (!this.range || !this.data) {
this.noDatapoints = true;
return; return;
} }
this.multipleValues = false; this.multipleValues = false;
if (!this.panel.useMax) { if (!this.panel.useMax) {
if (this.cardsData) { if (this.bucketMatrix) {
this.multipleValues = this.cardsData.multipleValues; this.multipleValues = this.bucketMatrix.multipleValues;
} }
} }
this.noColorDefined = false; this.noColorDefined = false;
if (this.panel.color.mode === 'discrete') { if (this.panel.color.mode === 'discrete') {
this.discreteHelper.updateCardsValuesHasColorInfo(); if (this.panel.seriesFilterIndex == -1) {
this.discreteExtraSeries.updateCardsValuesHasColorInfo();
if (this.cardsData) { } else {
this.noColorDefined = this.cardsData.noColorDefined; this.discreteExtraSeries.updateCardsValuesHasColorInfoSingle();
} }
if (this.bucketMatrix) {
this.noColorDefined = this.bucketMatrix.noColorDefined;
}
}
this.noDatapoints = false;
if (this.bucketMatrix) {
this.noDatapoints = this.bucketMatrix.noDatapoints;
} }
} }
}, { }, {
@@ -455,182 +592,228 @@ System.register(["lodash", "./color_legend", "app/core/utils/kbn", "app/plugins/
value: function postRefresh() { value: function postRefresh() {
this.noColorDefined = false; this.noColorDefined = false;
} }
}, {
key: "onEditorAddThreshold",
value: function onEditorAddThreshold() {
this.panel.color.thresholds.push({
color: this.panel.defaultColor
});
this.render();
}
}, {
key: "onEditorRemoveThreshold",
value: function onEditorRemoveThreshold(index) {
this.panel.color.thresholds.splice(index, 1);
this.render();
}
}, {
key: "onEditorRemoveThresholds",
value: function onEditorRemoveThresholds() {
this.panel.color.thresholds = [];
this.render();
}
}, {
key: "onEditorAddThreeLights",
value: function onEditorAddThreeLights() {
this.panel.color.thresholds.push({
color: "red",
value: 2,
tooltip: "error"
});
this.panel.color.thresholds.push({
color: "yellow",
value: 1,
tooltip: "warning"
});
this.panel.color.thresholds.push({
color: "green",
value: 0,
tooltip: "ok"
});
this.render();
}
/* https://ethanschoonover.com/solarized/ */
}, {
key: "onEditorAddSolarized",
value: function 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();
}
}, { }, {
key: "link", key: "link",
value: function link(scope, elem, attrs, ctrl) { value: function link(scope, elem, attrs, ctrl) {
rendering(scope, elem, attrs, ctrl); rendering(scope, elem, attrs, ctrl);
} // group values into buckets by target } // Compatible with Grafana 7.0 overrides feature.
}, { }, {
key: "convertToCards", key: "retrieveTimeVar",
value: function convertToCards(data) { value: function retrieveTimeVar() {
var cardsData = { var time = this.timeSrv.timeRangeForUrl();
cards: [], return 'from=' + time.from + '&to=' + time.to;
xBucketSize: 0, } // convertToBuckets groups values in data into buckets by target and timestamp.
yBucketSize: 0, //
maxValue: 0, // data is a result from datasource. It is an array of timeseries and tables:
minValue: 0,
multipleValues: false,
noColorDefined: false,
targets: [],
// array of available unique targets
targetIndex: {} // indices in data array for each of available unique targets
}; /* [
// timeseries
{
target: "query alias",
refId: "A",
datapoints: [
[0, 1582681239911],
[3, 158....],
...
],
tags?:{key: value,...}
},
// table
{
name: "table name",
refId: "B",
columns: [
{text: "id"},
{text: "info"},
...
],
rows: [
[1, "123"],
[2, "44411"],
...
]
},
...
]
to and from — a time range of the panel.
intervalMs — a calculated interval. It is used to split a time range.
*/
}, {
key: "convertDataToBuckets",
value: function convertDataToBuckets(data, from, to, intervalMs, mostRecentBucket) {
var _this4 = this;
var bucketMatrix = new BucketMatrix();
bucketMatrix.rangeMs = to - from;
bucketMatrix.intervalMs = intervalMs;
if (!data || data.length == 0) { if (!data || data.length == 0) {
return cardsData; // Mimic heatmap and graph 'no data' labels.
} // Collect uniq timestamps from data and spread over targets and timestamps bucketMatrix.targets = ["1.0", "0.0", "-1.0"];
// collect uniq targets and their indices bucketMatrix.buckets["1.0"] = [];
bucketMatrix.buckets["0.0"] = [];
bucketMatrix.buckets["-1.0"] = [];
_.map(data, function (d, i) { bucketMatrix.xBucketSize = 42;
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i); bucketMatrix.noDatapoints = true;
}); // TODO add some logic for targets heirarchy return bucketMatrix;
cardsData.targets = _.keys(cardsData.targetIndex);
cardsData.yBucketSize = cardsData.targets.length; // 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.
// 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 = new Card();
card.id = i * cardsData.xBucketSize + j;
card.values = [];
card.y = target;
card.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]];
if (s.datapoints.length <= j) {
continue;
}
var datapoint = s.datapoints[j];
if (card.values.length === 0) {
card.x = datapoint[TIME_INDEX];
}
card.values.push(datapoint[VALUE_INDEX]);
}
card.minValue = _.min(card.values);
card.maxValue = _.max(card.values);
if (card.values.length > 1) {
cardsData.multipleValues = true;
card.multipleValues = true;
card.value = card.maxValue; // max value by default
} else {
card.value = card.maxValue; // max value by default
}
if (cardsData.maxValue < card.maxValue) cardsData.maxValue = card.maxValue;
if (cardsData.minValue > card.minValue) cardsData.minValue = card.minValue;
if (card.x != -1) {
cardsData.cards.push(card);
}
}
} }
return cardsData; var targetIndex = {}; // Group indicies of elements in data by target (y label).
// lodash version:
//_.map(data, (d, i) => {
// targetIndex[d.target] = _.concat(_.toArray(targetIndex[d.target]), i);
//});
data.map(function (queryResult, i) {
var yLabel = queryResult.target;
if (!targetIndex.hasOwnProperty(yLabel)) {
targetIndex[yLabel] = [];
}
targetIndex[yLabel].push(i);
});
var targetKeys = _.keys(targetIndex); //console.log ("targetIndex: ", targetIndex, "targetKeys: ", targetKeys);
var targetTimestampRanges = {}; // Collect all timestamps for each target.
// Make map timestamp => [from, to]. from == previous ts, to == ts from datapoint.
targetKeys.map(function (target) {
var targetTimestamps = [];
for (var si = 0; si < targetIndex[target].length; si++) {
var s = data[targetIndex[target][si]];
_.map(s.datapoints, function (datapoint, idx) {
targetTimestamps.push(datapoint[TIME_INDEX] - from);
});
} //console.log("timestamps['"+target+"'] = ", targetTimestamps);
targetTimestamps = _.uniq(targetTimestamps); //console.log("uniq timestamps['"+target+"'] = ", targetTimestamps);
targetTimestampRanges[target] = [];
for (var i = targetTimestamps.length - 1; i >= 0; i--) {
var tsTo = targetTimestamps[i];
var tsFrom = 0;
if (tsTo < 0) {
tsFrom = tsTo - intervalMs;
} else {
if (i - 1 >= 0) {
// Set from to previous timestamp + 1ms;
tsFrom = targetTimestamps[i - 1] + 1; // tfTo - tfFrom should not be more than intervalMs
var minFrom = tsTo - intervalMs;
if (tsFrom < minFrom) {
tsFrom = minFrom;
}
}
}
targetTimestampRanges[target][tsTo] = [tsFrom, tsTo];
}
}); // console.log ("targetTimestampRanges: ", targetTimestampRanges);
// Create empty buckets using intervalMs to calculate ranges.
// If mostRecentBucket is set, create a bucket with a range "to":"to"
// to store most recent values.
targetKeys.map(function (target) {
var targetEmptyBuckets = [];
var lastTs = to - from;
if (mostRecentBucket) {
var topBucket = new Bucket();
topBucket.yLabel = target;
topBucket.relTo = lastTs;
topBucket.relFrom = lastTs;
topBucket.values = [];
topBucket.mostRecent = true;
if (targetTimestampRanges[target].hasOwnProperty(lastTs)) {
topBucket.relFrom = targetTimestampRanges[target][lastTs][0];
lastTs = topBucket.relFrom;
}
topBucket.to = topBucket.relTo + from;
topBucket.from = topBucket.relFrom + from;
targetEmptyBuckets.push(topBucket);
}
var idx = 0;
var bucketFrom = 0;
while (bucketFrom >= 0) {
var b = new Bucket();
b.yLabel = target;
b.relTo = lastTs - idx * intervalMs;
b.relFrom = lastTs - (idx + 1) * intervalMs;
b.to = b.relTo + from;
b.from = b.relFrom + from;
b.values = [];
bucketFrom = b.relFrom;
targetEmptyBuckets.push(b);
idx++;
}
targetEmptyBuckets.map(function (bucket, i) {
bucket.xid = i;
});
bucketMatrix.buckets[target] = targetEmptyBuckets;
}); //console.log ("bucketMatrix: ", bucketMatrix);
// Put values into buckets.
bucketMatrix.minValue = Number.MAX_VALUE;
bucketMatrix.maxValue = Number.MIN_SAFE_INTEGER;
targetKeys.map(function (target) {
targetIndex[target].map(function (dataIndex) {
var s = data[dataIndex];
s.datapoints.map(function (dp) {
for (var i = 0; i < bucketMatrix.buckets[target].length; i++) {
if (bucketMatrix.buckets[target][i].belong(dp[TIME_INDEX])) {
bucketMatrix.buckets[target][i].put(dp[VALUE_INDEX]);
}
}
});
});
bucketMatrix.buckets[target].map(function (bucket) {
bucket.minValue = _.min(bucket.values);
bucket.maxValue = _.max(bucket.values);
if (bucket.minValue < bucketMatrix.minValue) {
bucketMatrix.minValue = bucket.minValue;
}
if (bucket.maxValue > bucketMatrix.maxValue) {
bucketMatrix.maxValue = bucket.maxValue;
}
bucket.value = bucket.maxValue;
if (bucket.values.length > 1) {
bucketMatrix.multipleValues = true;
bucket.multipleValues = true;
bucket.value = _this4.panel.seriesFilterIndex != -1 ? bucket.values[_this4.panel.seriesFilterIndex] : bucket.maxValue;
}
});
});
bucketMatrix.xBucketSize = Number.MIN_SAFE_INTEGER;
targetKeys.map(function (target) {
var bucketsLen = bucketMatrix.buckets[target].length;
if (bucketsLen > bucketMatrix.xBucketSize) {
bucketMatrix.xBucketSize = bucketsLen;
}
}); //console.log ("bucketMatrix with values: ", bucketMatrix);
bucketMatrix.targets = targetKeys;
return bucketMatrix;
} }
}]); }]);
+1 -1
View File
File diff suppressed because one or more lines are too long
+98 -3
View File
@@ -13,7 +13,8 @@ System.register(["app/core/utils/kbn"], function (_export, _context) {
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function statusHeatmapOptionsEditor() { /** @ngInject */
function optionsEditorCtrl() {
'use strict'; 'use strict';
return { return {
@@ -24,7 +25,7 @@ System.register(["app/core/utils/kbn"], function (_export, _context) {
}; };
} }
_export("statusHeatmapOptionsEditor", statusHeatmapOptionsEditor); _export("optionsEditorCtrl", optionsEditorCtrl);
return { return {
setters: [function (_appCoreUtilsKbn) { setters: [function (_appCoreUtilsKbn) {
@@ -34,6 +35,9 @@ System.register(["app/core/utils/kbn"], function (_export, _context) {
_export("StatusHeatmapOptionsEditorCtrl", StatusHeatmapOptionsEditorCtrl = _export("StatusHeatmapOptionsEditorCtrl", StatusHeatmapOptionsEditorCtrl =
/*#__PURE__*/ /*#__PURE__*/
function () { function () {
StatusHeatmapOptionsEditorCtrl.$inject = ["$scope"];
/** @ngInject */
function StatusHeatmapOptionsEditorCtrl($scope) { function StatusHeatmapOptionsEditorCtrl($scope) {
_classCallCheck(this, StatusHeatmapOptionsEditorCtrl); _classCallCheck(this, StatusHeatmapOptionsEditorCtrl);
@@ -47,7 +51,6 @@ System.register(["app/core/utils/kbn"], function (_export, _context) {
this.panelCtrl = $scope.ctrl; this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel; this.panel = this.panelCtrl.panel;
this.unitFormats = kbn.getUnitFormats(); this.unitFormats = kbn.getUnitFormats();
this.panelCtrl.render();
} }
_createClass(StatusHeatmapOptionsEditorCtrl, [{ _createClass(StatusHeatmapOptionsEditorCtrl, [{
@@ -56,6 +59,98 @@ System.register(["app/core/utils/kbn"], function (_export, _context) {
this.panel.data.unitFormat = subItem.value; this.panel.data.unitFormat = subItem.value;
this.panelCtrl.render(); this.panelCtrl.render();
} }
}, {
key: "render",
value: function render() {
this.panelCtrl.render();
}
}, {
key: "onAddThreshold",
value: function onAddThreshold() {
this.panel.color.thresholds.push({
color: this.panel.defaultColor
});
this.render();
}
}, {
key: "onRemoveThreshold",
value: function onRemoveThreshold(index) {
this.panel.color.thresholds.splice(index, 1);
this.render();
}
}, {
key: "onRemoveThresholds",
value: function onRemoveThresholds() {
this.panel.color.thresholds = [];
this.render();
}
}, {
key: "onAddThreeLights",
value: function onAddThreeLights() {
this.panel.color.thresholds.push({
color: "red",
value: 2,
tooltip: "error"
});
this.panel.color.thresholds.push({
color: "yellow",
value: 1,
tooltip: "warning"
});
this.panel.color.thresholds.push({
color: "green",
value: 0,
tooltip: "ok"
});
this.render();
}
/* https://ethanschoonover.com/solarized/ */
}, {
key: "onAddSolarized",
value: function onAddSolarized() {
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();
}
}]); }]);
return StatusHeatmapOptionsEditorCtrl; return StatusHeatmapOptionsEditorCtrl;
+1 -1
View File
File diff suppressed because one or more lines are too long
+165
View File
@@ -0,0 +1,165 @@
"use strict";
System.register(["lodash"], function (_export, _context) {
"use strict";
var _;
function migrate_V0_V1(panel) {
// Remove unused fields.
if (_.has(panel, "xAxis.labelFormat")) {
delete panel.xAxis.labelFormat;
}
if (_.has(panel, "xAxis.minBucketWidthToShowWeekends")) {
delete panel.xAxis.minBucketWidthToShowWeekends;
}
if (_.has(panel, "xAxis.showCrosshair")) {
delete panel.xAxis.showCrosshair;
}
if (_.has(panel, "xAxis.showWeekends")) {
delete panel.xAxis.showWeekends;
}
if (_.has(panel, "yAxis.showCrosshair")) {
delete panel.yAxis.showCrosshair;
}
if (_.has(panel, "data.unitFormat")) {
delete panel.data;
} // Migrate cardSpacing value. Seems rare (update from version 0.0.2).
if (_.has(panel, "cards.cardSpacing")) {
if (!_.has(panel, "cards.cardVSpacing")) {
if (panel.cards.cardSpacing) {
panel.cards.cardVSpacing = panel.cards.cardSpacing;
panel.cards.cardHSpacing = panel.cards.cardSpacing;
}
}
delete panel.cards.cardSpacing;
} // Migrate initial config for urls in tooltip (pull/86).
// 'usingUrl' was used to show tooltip with urls on click or not.
if (_.has(panel, "usingUrl")) {
if (!_.has(panel, "tooltip.freezeOnClick")) {
panel.tooltip.freezeOnClick = panel.usingUrl;
}
delete panel.usingUrl;
} // 'urls' array is now tooltip.items array. Also items are changed.
if (_.has(panel, "urls")) {
if (!_.has(panel, "tooltip.items")) {
panel.tooltip.items = [];
var hasRealItems = true;
if (panel.urls.length == 0) {
hasRealItems = false;
}
if (panel.urls.length == 1) {
var url = panel.urls[0];
if (url.base_url === '' && url.label === '') {
hasRealItems = false;
}
}
if (hasRealItems) {
panel.tooltip.showItems = true;
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = panel.urls[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var _url = _step.value;
var item = {
urlTemplate: _.toString(_url.base_url),
urlText: _.toString(_url.label),
urlIcon: _.toString(_url.icon_fa),
urlToLowerCase: _url.forcelowercase,
valueDateFormat: ''
}; // replace $vars with new ${__vars} if url template is not empty
if (item.urlTemplate !== "") {
// $time was a graph time with prepended &
item.urlTemplate = _.replace(_url.base_url, /\$time/g, "&${__url_time_range}"); // $series_label was a y axis label
item.urlTemplate = _.replace(item.urlTemplate, /\$series_label/, "${__y_label}"); // $series_extra was a value from bucket. This value has format options and index.
var valueVar = "__value";
if (_url.useExtraSeries === true) {
// index?
if (_url.extraSeries.index > -1) {
valueVar += "_" + _url.extraSeries.index;
}
var format = _.toString(_url.extraSeries.format);
if (format === 'YYYY/MM/DD/HH_mm_ss') {
valueVar += '_date';
item.valueDateFormat = format;
}
}
item.urlTemplate = _.replace(item.urlTemplate, /\$series_extra/, "${".concat(valueVar, "}"));
}
panel.tooltip.items.push(item);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator["return"] != null) {
_iterator["return"]();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
}
delete panel.urls;
} // create statusmap metadata
panel.statusmap = {
"ConfigVersion": "v1"
};
}
function migratePanelConfig(panel) {
if (_.has(panel, "statusmap")) {
if (panel.statusmap.ConfigVersion == "v1") {
return;
}
} else {
migrate_V0_V1(panel);
}
return;
}
_export("migratePanelConfig", migratePanelConfig);
return {
setters: [function (_lodash) {
_ = _lodash.default;
}],
execute: function () {}
};
});
//# sourceMappingURL=panel_config_migration.js.map
File diff suppressed because one or more lines are too long
+50 -21
View File
@@ -81,9 +81,7 @@
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label"> <label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)"> <i class="fa fa-trash pointer" ng-click="editor.onRemoveThreshold($index)"></i>
<i class="fa fa-trash"/>
</a>
</label> </label>
</div> </div>
</div> </div>
@@ -91,12 +89,12 @@
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form"></div> <div class="gf-form"></div>
<div class="gf-form"> <div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorAddThreshold()"> <button class="btn btn-inverse" ng-click="editor.onAddThreshold()">
<i class="fa fa-plus"></i> Add new status <i class="fa fa-plus"></i> Add new status
</button> </button>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorRemoveThresholds()"> <button class="btn btn-inverse" ng-click="editor.onRemoveThresholds()">
<i class="fa fa-minus"></i> Remove all <i class="fa fa-minus"></i> Remove all
</button> </button>
</div> </div>
@@ -108,12 +106,12 @@
<label class="gf-form-label">Presets</label> <label class="gf-form-label">Presets</label>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddThreeLights()"> <button class="btn" ng-click="editor.onAddThreeLights()">
Red-Yellow-Green Red-Yellow-Green
</button> </button>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddSolarized()"> <button class="btn" ng-click="editor.onAddSolarized()">
Solarized Solarized
</button> </button>
</div> </div>
@@ -145,11 +143,24 @@
checked="ctrl.panel.legend.show" on-change="ctrl.render()"> checked="ctrl.panel.legend.show" on-change="ctrl.render()">
</gf-form-switch> </gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-8" <gf-form-switch class="gf-form" label-class="width-8"
label="Show tooltip" label="Show X axis"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()"> checked="ctrl.panel.xAxis.show" on-change="ctrl.render()">
</gf-form-switch> </gf-form-switch>
<div class="gf-form"> <gf-form-switch class="gf-form" label-class="width-8"
<label class="gf-form-label width-8">Y axis sort</label> label="Show Y axis"
checked="ctrl.panel.yAxis.show" on-change="ctrl.render()">
</gf-form-switch>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Min width</label>
<input type="number" ng-model="ctrl.panel.yAxis.minWidth" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.render()" ng-model-onblur>
</div>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Max width</label>
<input type="number" ng-model="ctrl.panel.yAxis.maxWidth" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.render()" ng-model-onblur>
</div>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Rows sort</label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input max-width-8" <select class="gf-form-input max-width-8"
ng-model="ctrl.panel.yAxisSort" ng-model="ctrl.panel.yAxisSort"
@@ -157,46 +168,64 @@
ng-change="ctrl.render()"></select> ng-change="ctrl.render()"></select>
</div> </div>
</div> </div>
</div>
</div>
<div class="section gf-form-group"> <div class="section gf-form-group">
<h5 class="section-heading">Bucket</h5> <h5 class="section-heading">Buckets</h5>
<gf-form-switch class="gf-form" label-class="width-9" <gf-form-switch class="gf-form" label-class="width-8"
label="Multiple values" label="Multiple values"
checked="ctrl.panel.useMax" on-change="ctrl.render()"> checked="ctrl.panel.useMax" on-change="ctrl.render()">
</gf-form-switch> </gf-form-switch>
<div class="gf-form" ng-show="ctrl.panel.useMax"> <div class="gf-form" ng-show="ctrl.panel.useMax">
Color for bucket with multiple values is determined by color mode<br/> Color for bucket with multiple values is determined by color mode<br/>
</div> </div>
<div class="gf-form">
<label class="gf-form-label width-8">Values index</label>
<input type="number" class="gf-form-input width-5" placeholder="-1" data-placement="right"
bs-tooltip="'Display only values with this index in a bucket. Use -1 to display all values'"
ng-model="ctrl.panel.seriesFilterIndex" ng-change="ctrl.refresh()"
ng-model-onblur>
</div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">Display nulls</label> <label class="gf-form-label width-8">Display nulls</label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input max-width-9" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['as empty', 'as zero']" ng-change="ctrl.render()"></select> <select class="gf-form-input max-width-9" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['as empty', 'as zero']" ng-change="ctrl.render()"></select>
</div> </div>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">Min width for 1/1</label> <label class="gf-form-label width-8">Min width</label>
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur> <input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">V spacing</label> <label class="gf-form-label width-8">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> <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>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">H spacing</label> <label class="gf-form-label width-8">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> <input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards horizontal spacing in pixels'" ng-model="ctrl.panel.cards.cardHSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">Rounding</label> <label class="gf-form-label width-8">Rounding</label>
<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> <input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Cards rounding radius in pixels'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
</div> </div>
</div> </div>
<div class="section gf-form-group"> <div class="section gf-form-group">
<h5 class="section-heading">Pagination</h5>
<div class="gf-form">
<gf-form-switch class="gf-form" label="Enable pagination" label-class="width-12"
checked="ctrl.panel.usingPagination" on-change="ctrl.render()"></gf-form-switch>
</div>
<div class="gf-form" ng-if="ctrl.panel.usingPagination">
<label class="gf-form-label width-10">Rows per page</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="top"
bs-tooltip="'Number of rows to show by default'" ng-model="ctrl.panel.pageSize" ng-change="ctrl.changeDefaultPaginationSize(ctrl.panel.pageSize)"
ng-model-onblur>
</div>
</div> </div>
<div class="section gf-form-group">
</div>
</div> </div>
+119
View File
@@ -0,0 +1,119 @@
<div class="editor-row">
<div class="section gf-form-group">
<gf-form-switch class="gf-form" label-class="width-10"
label="Show tooltip"
tooltip="'Show tooltip when mouse is over the card'"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-10"
label="Freeze on click"
tooltip="'Freeze tooltip copy when card is clicked'"
checked="ctrl.panel.tooltip.freezeOnClick" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-10"
label="Show items"
tooltip="'Show items (urls) in tooltip'"
checked="ctrl.panel.tooltip.showItems" on-change="ctrl.render()">
</gf-form-switch>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Items</h5>
<div class="gf-form" ng-show="ctrl.panel.tooltip.items.length == 0">
<label class="gf-form-label width-2">0</label>
<label class="gf-form-input width-30">No items defined.</label>
</div>
</div>
</div>
<div>
<div ng-repeat="item in ctrl.panel.tooltip.items">
<div class="gf-form gf-form--grow">
<label class="gf-form-label width-6">
URL
<info-popover mode="left-normal">
<p>Specify an URL (relative or absolute)</p>
<span>
Use special variables:
<br>
<em>${__y_label}</em> name of the time series (alias or legend in the query editor).
<br>
<em>${__url_time_range}</em> refers to graph range in the form of 'from=...&to=...'.
<br>
<em>${__value}</em> refers to current bucket value or max value if multiple values mode is on.
<br>
<em>${__value_N}</em> refers to Nth value in current bucket. Value indexes are started from 0. For instance,
<em>${__value_1}</em> refers to second value.
<br>
<em>${__value_date}</em> refers to __value formatted as date.
<em>${__value_N_date}</em> refers to Nth value in current bucket formatted as date.
</span>
</info-popover>
</label>
<input type="text" class="gf-form-input" placeholder="https://www.google.es" data-placement="top"
bs-tooltip="'This is the url to be shown when the user clicks on it'" ng-model="item.urlTemplate"
ng-change="ctrl.refresh()">
<label class="gf-form-label">
<i class="fa fa-trash pointer" ng-click="editor.onRemoveUrl($index)"></i>
</label>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-4">Icon</label>
<label class="gf-form-label width-2">
<i class="fa fa-{{item.urlIcon}}"></i>
</label>
<input
class="gf-form-input width-10"
type="text"
ng-model="item.urlIcon"
bs-typeahead="editor.getFAIconClasses"
bs-tooltip="'The icon for URL link'"
data-placement="top"
ng-blur="ctrl.render()"
data-min-length="0"
data-items="100"
/>
</div>
<div class="gf-form">
<label class="gf-form-label width-4">Label</label>
<input type="text" class="gf-form-input width-16" placeholder="My URL" data-placement="top"
bs-tooltip="'Text for URL link'"
ng-model="item.urlText" ng-change="ctrl.refresh()">
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label-class="width-8" label="Lowercase link" checked="item.urlToLowerCase"
on-change="ctrl.render()">
</gf-form-switch>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Date format</label>
<input type="text" class="gf-form-input width-12" placeholder="YYYY/MM/DD/HH_mm_ss"
data-placement="top" bs-tooltip="'Date format for __value_date'"
bs-typeahead="editor.getValueDateFormats"
data-min-length="0"
data-items="100"
ng-model="item.valueDateFormat"
ng-blur="ctrl.render()">
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form"></div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="editor.onAddUrl()">
<i class="fa fa-plus"></i> Add URL
</button>
</div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="editor.onRemoveUrls()">
<i class="fa fa-minus"></i> Remove all
</button>
</div>
</div>
</div>
+5 -5
View File
@@ -9,7 +9,7 @@
"name": "Flant JSC", "name": "Flant JSC",
"url": "http://flant.com" "url": "http://flant.com"
}, },
"keywords": ["status", "heatmap", "panel"], "keywords": ["status", "statusmap", "heatmap", "panel"],
"logos": { "logos": {
"small": "img/logo.svg", "small": "img/logo.svg",
"large": "img/logo.svg" "large": "img/logo.svg"
@@ -17,7 +17,6 @@
"links": [ "links": [
{"name": "Github", "url": "https://github.com/flant/grafana-statusmap"}, {"name": "Github", "url": "https://github.com/flant/grafana-statusmap"},
{"name": "MIT Licence", "url": "https://github.com/flant/grafana-statusmap/blob/master/LICENSE"} {"name": "MIT Licence", "url": "https://github.com/flant/grafana-statusmap/blob/master/LICENSE"}
], ],
"screenshots": [ "screenshots": [
{"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"}, {"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"},
@@ -25,12 +24,13 @@
{"name": "Color mapping", "path": "img/color-mapping.png"}, {"name": "Color mapping", "path": "img/color-mapping.png"},
{"name": "Queries example", "path": "img/queries-example.png"} {"name": "Queries example", "path": "img/queries-example.png"}
], ],
"version": "0.2.0", "version": "0.3.2",
"updated": "2019-12-03" "updated": "2020-08-13"
}, },
"dependencies": { "dependencies": {
"grafanaVersion": "5.1.x", "grafanaDependency": ">=5.4.3",
"grafanaVersion": "5.4.x",
"plugins": [ ] "plugins": [ ]
} }
} }
+130 -73
View File
@@ -1,16 +1,16 @@
"use strict"; "use strict";
System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/core", "d3", "./libs/d3-scale-chromatic/index", "./tooltip", "./annotations"], function (_export, _context) { System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/core", "d3", "./libs/d3-scale-chromatic/index", "./tooltip", "./annotations", "./module", "./libs/grafana/events/index"], function (_export, _context) {
"use strict"; "use strict";
var _, $, moment, kbn, appEvents, contextSrv, d3, d3ScaleChromatic, StatusmapTooltip, 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, StatusmapRenderer; var _, $, moment, kbn, appEvents, contextSrv, d3, d3ScaleChromatic, StatusmapTooltip, AnnotationTooltip, renderComplete, CoreEvents, PanelEvents, 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, Statusmap, StatusmapRenderer;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } } 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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function rendering(scope, elem, attrs, ctrl) { function rendering(scope, elem, attrs, ctrl) {
@@ -68,6 +68,11 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
StatusmapTooltip = _tooltip.StatusmapTooltip; StatusmapTooltip = _tooltip.StatusmapTooltip;
}, function (_annotations) { }, function (_annotations) {
AnnotationTooltip = _annotations.AnnotationTooltip; AnnotationTooltip = _annotations.AnnotationTooltip;
}, function (_module) {
renderComplete = _module.renderComplete;
}, function (_libsGrafanaEventsIndex) {
CoreEvents = _libsGrafanaEventsIndex.CoreEvents;
PanelEvents = _libsGrafanaEventsIndex.PanelEvents;
}], }],
execute: function () { execute: function () {
MIN_CARD_SIZE = 5; MIN_CARD_SIZE = 5;
@@ -81,6 +86,21 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
Y_AXIS_TICK_PADDING = 5; Y_AXIS_TICK_PADDING = 5;
MIN_SELECTION_WIDTH = 2; MIN_SELECTION_WIDTH = 2;
Statusmap = function Statusmap() {
_classCallCheck(this, Statusmap);
_defineProperty(this, "$svg", void 0);
_defineProperty(this, "svg", void 0);
_defineProperty(this, "bucketMatrix", void 0);
_defineProperty(this, "timeRange", {
from: 0,
to: 0
});
};
_export("StatusmapRenderer", StatusmapRenderer = _export("StatusmapRenderer", StatusmapRenderer =
/*#__PURE__*/ /*#__PURE__*/
function () { function () {
@@ -131,9 +151,9 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
_defineProperty(this, "yGridSize", 0); _defineProperty(this, "yGridSize", 0);
_defineProperty(this, "data", void 0); _defineProperty(this, "bucketMatrix", void 0);
_defineProperty(this, "cardsData", void 0); _defineProperty(this, "bucketMatrixPager", void 0);
_defineProperty(this, "panel", void 0); _defineProperty(this, "panel", void 0);
@@ -158,7 +178,7 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
_defineProperty(this, "dataRangeWidingFactor", DATA_RANGE_WIDING_FACTOR); _defineProperty(this, "dataRangeWidingFactor", DATA_RANGE_WIDING_FACTOR);
// $heatmap is JQuery object, but heatmap is D3 // $heatmap is JQuery object, but heatmap is D3
this.$heatmap = this.elem.find('.status-heatmap-panel'); this.$heatmap = this.elem.find('.statusmap-panel');
this.tooltip = new StatusmapTooltip(this.$heatmap, this.scope); this.tooltip = new StatusmapTooltip(this.$heatmap, this.scope);
this.annotationTooltip = new AnnotationTooltip(this.$heatmap, this.scope); this.annotationTooltip = new AnnotationTooltip(this.$heatmap, this.scope);
this.yOffset = 0; this.yOffset = 0;
@@ -179,18 +199,19 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
top: 10, top: 10,
bottom: 20 bottom: 20
}; };
this.ctrl.events.on('render', this.onRender.bind(this)); this.ctrl.events.on(PanelEvents.render, this.onRender.bind(this));
this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this); ///////////////////////////// this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this); /////////////////////////////
// Selection and crosshair // // Selection and crosshair //
///////////////////////////// /////////////////////////////
// Shared crosshair and tooltip // Shared crosshair and tooltip this.empty = true;
appEvents.on('graph-hover', this.onGraphHover.bind(this), this.scope); appEvents.on(CoreEvents.graphHover, this.onGraphHover.bind(this), this.scope);
appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), this.scope); // Register selection listeners appEvents.on(CoreEvents.graphHoverClear, this.onGraphHoverClear.bind(this), this.scope); // Register selection listeners
this.$heatmap.on('mousedown', this.onMouseDown.bind(this)); this.$heatmap.on('mousedown', this.onMouseDown.bind(this));
this.$heatmap.on('mousemove', this.onMouseMove.bind(this)); this.$heatmap.on('mousemove', this.onMouseMove.bind(this));
this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this)); this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this));
this.$heatmap.on('click', this.onMouseClick.bind(this));
} }
_createClass(StatusmapRenderer, [{ _createClass(StatusmapRenderer, [{
@@ -219,7 +240,14 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
height = parseInt(height.replace('px', ''), 10); height = parseInt(height.replace('px', ''), 10);
} }
height -= this.panel.legend.show ? 32 : 10; // bottom padding and space for legend. Change margin in .status-heatmap-color-legend ! if (this.panel.usingPagination) {
// TODO get height of pagination controls.
// reserve height for legend and for a row of pagination controls.
height -= this.panel.legend.show ? 70 : 40; // bottom padding and space for legend. Change margin in .status-heatmap-color-legend !
} else {
// reserve height for legend
height -= this.panel.legend.show ? 32 : 4; // bottom padding and space for legend. Change margin in .status-heatmap-color-legend !
}
this.$heatmap.css('height', height + 'px'); this.$heatmap.css('height', height + 'px');
return true; return true;
@@ -258,6 +286,10 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
key: "addXAxis", key: "addXAxis",
value: function addXAxis() { value: function addXAxis() {
// Scale timestamps to cards centers // Scale timestamps to cards centers
//this.scope.xScale = this.xScale = d3.scaleTime()
// .domain([this.timeRange.from, this.timeRange.to])
// .range([this.xGridSize/2, this.chartWidth-this.xGridSize/2]);
// Buckets without the most recent
this.scope.xScale = this.xScale = d3.scaleTime().domain([this.timeRange.from, this.timeRange.to]).range([this.xGridSize / 2, this.chartWidth - this.xGridSize / 2]); this.scope.xScale = this.xScale = d3.scaleTime().domain([this.timeRange.from, this.timeRange.to]).range([this.xGridSize / 2, this.chartWidth - this.xGridSize / 2]);
var ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX; var ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
var grafanaTimeFormatter = grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to); var grafanaTimeFormatter = grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
@@ -311,14 +343,7 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
}, { }, {
key: "addYAxis", key: "addYAxis",
value: function addYAxis() { value: function addYAxis() {
var ticks = _.uniq(_.map(this.data, function (d) { var ticks = this.bucketMatrixPager.targets(); // TODO move sorting into bucketMatrixPager.
return d.target;
})); // Set default Y min and max if no data
if (_.isEmpty(this.data)) {
ticks = [''];
}
if (this.panel.yAxisSort == 'a → z') { if (this.panel.yAxisSort == 'a → z') {
ticks.sort(function (a, b) { ticks.sort(function (a, b) {
@@ -386,17 +411,19 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
// calculate sizes for cards drawing // calculate sizes for cards drawing
}, { }, {
key: "addHeatmapCanvas", key: "addStatusmapCanvas",
value: function addHeatmapCanvas() { value: function addStatusmapCanvas() {
var heatmap_elem = this.$heatmap[0]; var heatmap_elem = this.$heatmap[0];
this.width = Math.floor(this.$heatmap.width()) - this.padding.right; this.width = Math.floor(this.$heatmap.width()) - this.padding.right;
this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom; this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom;
if (this.heatmap) { if (this.heatmap) {
this.heatmap.remove(); this.heatmap.remove();
} } // Insert svg as a first child into heatmap_elem
// as the frozen tooltip moving logiс assumes that tooltip is the last child.
this.heatmap = d3.select(heatmap_elem).append("svg").attr("width", this.width).attr("height", this.height);
this.heatmap = d3.select(heatmap_elem).insert("svg", ":first-child").attr("width", this.width).attr("height", this.height);
this.chartHeight = this.height - this.margin.top - this.margin.bottom; this.chartHeight = this.height - this.margin.top - this.margin.bottom;
this.chartTop = this.margin.top; this.chartTop = this.margin.top;
this.chartBottom = this.chartTop + this.chartHeight; this.chartBottom = this.chartTop + this.chartHeight;
@@ -404,7 +431,12 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
this.cardVSpacing = this.panel.cards.cardVSpacing !== null ? this.panel.cards.cardVSpacing : CARD_V_SPACING; this.cardVSpacing = this.panel.cards.cardVSpacing !== null ? this.panel.cards.cardVSpacing : CARD_V_SPACING;
this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND; // calculate yOffset for YAxis this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND; // calculate yOffset for YAxis
this.yGridSize = Math.floor(this.chartHeight / this.cardsData.yBucketSize); this.yGridSize = this.chartHeight;
if (this.bucketMatrixPager.targets().length > 0) {
this.yGridSize = Math.floor(this.chartHeight / this.bucketMatrixPager.targets().length);
}
this.cardHeight = this.yGridSize ? this.yGridSize - this.cardVSpacing : 0; this.cardHeight = this.yGridSize ? this.yGridSize - this.cardVSpacing : 0;
this.yOffset = this.cardHeight / 2; this.yOffset = this.cardHeight / 2;
this.addYAxis(); this.addYAxis();
@@ -412,7 +444,7 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
this.chartWidth = this.width - this.yAxisWidth - this.margin.right; // TODO allow per-y cardWidth! this.chartWidth = this.width - this.yAxisWidth - this.margin.right; // TODO allow per-y cardWidth!
// we need to fill chartWidth with xBucketSize cards. // we need to fill chartWidth with xBucketSize cards.
this.xGridSize = this.chartWidth / (this.cardsData.xBucketSize + 1); this.xGridSize = this.chartWidth / (this.bucketMatrix.xBucketSize + 1);
this.cardWidth = this.xGridSize - this.cardHSpacing; this.cardWidth = this.xGridSize - this.cardHSpacing;
this.addXAxis(); this.addXAxis();
this.xAxisHeight = this.getXAxisHeight(this.heatmap); this.xAxisHeight = this.getXAxisHeight(this.heatmap);
@@ -426,27 +458,34 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
} }
} }
}, { }, {
key: "addHeatmap", key: "addStatusmap",
value: function addHeatmap() { value: function addStatusmap() {
var _this = this; var _this = this;
this.addHeatmapCanvas(); var maxValue = this.panel.color.max || this.bucketMatrix.maxValue;
var maxValue = this.panel.color.max || this.cardsData.maxValue; var minValue = this.panel.color.min || this.bucketMatrix.minValue;
var minValue = this.panel.color.min || this.cardsData.minValue;
if (this.panel.color.mode !== 'discrete') { if (this.panel.color.mode !== 'discrete') {
this.colorScale = this.getColorScale(maxValue, minValue); this.colorScale = this.getColorScale(maxValue, minValue);
} }
this.setOpacityScale(maxValue); this.setOpacityScale(maxValue); // Draw cards from buckets.
var cards = this.heatmap.selectAll(".status-heatmap-card").data(this.cardsData.cards);
cards.append("title"); this.heatmap.selectAll(".statusmap-cards-row").data(this.bucketMatrixPager.targets()).enter().selectAll(".statustmap-card").data(function (target) {
cards = cards.enter().append("rect").attr("cardId", function (c) { return _this.bucketMatrix.buckets[target];
return c.id; }).enter().append("rect").attr("cardId", function (b) {
}).attr("x", this.getCardX.bind(this)).attr("width", this.getCardWidth.bind(this)).attr("y", this.getCardY.bind(this)).attr("height", this.getCardHeight.bind(this)).attr("rx", this.cardRound).attr("ry", this.cardRound).attr("class", "bordered status-heatmap-card").style("fill", this.getCardColor.bind(this)).style("stroke", this.getCardColor.bind(this)).style("stroke-width", 0) //.style("stroke-width", getCardStrokeWidth) return b.id;
}).attr("xid", function (b) {
return b.xid;
}).attr("yid", function (b) {
return b.yLabel;
}).attr("x", this.getCardX.bind(this)).attr("width", this.getCardWidth.bind(this)).attr("y", this.getCardY.bind(this)).attr("height", this.getCardHeight.bind(this)).attr("rx", this.cardRound).attr("ry", this.cardRound).attr("class", function (b) {
return b.isEmpty() ? "empty-card" : "bordered statusmap-card";
}).style("fill", this.getCardColor.bind(this)).style("stroke", this.getCardColor.bind(this)).style("stroke-width", 0) //.style("stroke-width", getCardStrokeWidth)
//.style("stroke-dasharray", "3,3") //.style("stroke-dasharray", "3,3")
.style("opacity", this.getCardOpacity.bind(this)); .style("opacity", this.getCardOpacity.bind(this)); // Set mouse events on cards.
var $cards = this.$heatmap.find(".status-heatmap-card");
var $cards = this.$heatmap.find(".statusmap-card + .bordered");
$cards.on("mouseenter", function (event) { $cards.on("mouseenter", function (event) {
_this.tooltip.mouseOverBucket = true; _this.tooltip.mouseOverBucket = true;
@@ -459,7 +498,7 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
this._renderAnnotations(); this._renderAnnotations();
this.ctrl.events.emit('render-complete', { this.ctrl.events.emit(renderComplete, {
"chartWidth": this.chartWidth "chartWidth": this.chartWidth
}); });
} }
@@ -485,10 +524,7 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
var colorScheme = _.find(this.ctrl.colorSchemes, { var colorScheme = _.find(this.ctrl.colorSchemes, {
value: this.panel.color.colorScheme value: this.panel.color.colorScheme
}); // if (!colorScheme) { });
//
// }
var colorInterpolator = d3ScaleChromatic[colorScheme.value]; var colorInterpolator = d3ScaleChromatic[colorScheme.value];
var colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === 'dark' && !contextSrv.user.lightTheme; var colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === 'dark' && !contextSrv.user.lightTheme;
@@ -508,10 +544,12 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
} }
}, { }, {
key: "getCardX", key: "getCardX",
value: function getCardX(d) { value: function getCardX(b) {
var x; // cx is the center of the card. Card should be placed to the left. var x; // cx is the center of the card. Card should be placed to the left.
//let cx = this.xScale(d.x);
var cx = this.xScale(d.x); var rightX = b.relTo / this.bucketMatrix.rangeMs * this.chartWidth;
var cx = rightX - this.cardWidth / 2;
if (cx - this.cardWidth / 2 < 0) { if (cx - this.cardWidth / 2 < 0) {
x = this.yAxisWidth + this.cardHSpacing / 2; x = this.yAxisWidth + this.cardHSpacing / 2;
@@ -524,9 +562,11 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
}, { }, {
key: "getCardWidth", key: "getCardWidth",
value: function getCardWidth(d) { value: function getCardWidth(b) {
//return 20;
var w; var w;
var cx = this.xScale(d.x); var rightX = b.relTo / this.bucketMatrix.rangeMs * this.chartWidth;
var cx = rightX - this.cardWidth / 2; //let cx = this.xScale(d.x);
if (cx < this.cardWidth / 2) { if (cx < this.cardWidth / 2) {
// Center should not exceed half of card. // Center should not exceed half of card.
@@ -548,16 +588,19 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
} }
return w; return w;
} } // Top y for card.
// yScale gives ???
}, { }, {
key: "getCardY", key: "getCardY",
value: function getCardY(d) { value: function getCardY(b) {
return this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardVSpacing / 2; return this.yScale(b.yLabel) + this.chartTop - this.cardHeight - this.cardVSpacing / 2;
} }
}, { }, {
key: "getCardHeight", key: "getCardHeight",
value: function getCardHeight(d) { value: function getCardHeight(b) {
var ys = this.yScale(d.y); //return 20;
var ys = this.yScale(b.yLabel);
var y = ys + this.chartTop - this.cardHeight - this.cardVSpacing / 2; var y = ys + this.chartTop - this.cardHeight - this.cardVSpacing / 2;
var h = this.cardHeight; // Cut card height to prevent overlay var h = this.cardHeight; // Cut card height to prevent overlay
@@ -582,31 +625,35 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
} }
}, { }, {
key: "getCardColor", key: "getCardColor",
value: function getCardColor(d) { value: function getCardColor(bucket) {
if (this.panel.color.mode === 'opacity') { if (this.panel.color.mode === 'opacity') {
return this.panel.color.cardColor; return this.panel.color.cardColor;
} else if (this.panel.color.mode === 'spectrum') { } else if (this.panel.color.mode === 'spectrum') {
return this.colorScale(d.value); return this.colorScale(bucket.value);
} else if (this.panel.color.mode === 'discrete') { } else if (this.panel.color.mode === 'discrete') {
return this.ctrl.discreteHelper.getBucketColor(d.values); if (this.panel.seriesFilterIndex != null && this.panel.seriesFilterIndex != -1) {
return this.ctrl.discreteExtraSeries.getBucketColorSingle(bucket.values[this.panel.seriesFilterIndex]);
} else {
return this.ctrl.discreteExtraSeries.getBucketColor(bucket.values);
}
} }
} }
}, { }, {
key: "getCardOpacity", key: "getCardOpacity",
value: function getCardOpacity(d) { value: function getCardOpacity(bucket) {
if (this.panel.nullPointMode === 'as empty' && d.value == null) { if (this.panel.nullPointMode === 'as empty' && bucket.value == null) {
return 0; return 0;
} }
if (this.panel.color.mode === 'opacity') { if (this.panel.color.mode === 'opacity') {
return this.opacityScale(d.value); return this.opacityScale(bucket.value);
} else { } else {
return 1; return 1;
} }
} }
}, { }, {
key: "getCardStrokeWidth", key: "getCardStrokeWidth",
value: function getCardStrokeWidth(d) { value: function getCardStrokeWidth(b) {
if (this.panel.color.mode === 'discrete') { if (this.panel.color.mode === 'discrete') {
return '1'; return '1';
} }
@@ -663,9 +710,10 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
} }
}, { }, {
key: "onMouseLeave", key: "onMouseLeave",
value: function onMouseLeave() { value: function onMouseLeave(e) {
appEvents.emit('graph-hover-clear'); appEvents.emit(CoreEvents.graphHoverClear);
this.clearCrosshair(); //annotationTooltip.destroy(); this.clearCrosshair();
this.annotationTooltip.destroy();
} }
}, { }, {
key: "onMouseMove", key: "onMouseMove",
@@ -687,10 +735,18 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
//const pos = this.getEventPos(event, offset); //const pos = this.getEventPos(event, offset);
this.emitGraphHoverEvent(event); this.emitGraphHoverEvent(event);
this.drawCrosshair(offset.x); this.drawCrosshair(offset.x);
this.tooltip.show(event); //, data); // pos, this.data this.tooltip.show(event);
this.annotationTooltip.show(event); this.annotationTooltip.show(event);
} }
} // TODO emit an event and move logic to panelCtrl
}, {
key: "onMouseClick",
value: function onMouseClick(e) {
if (this.ctrl.panel.tooltip.freezeOnClick) {
this.tooltip.showFrozen(e);
this.tooltip.destroy();
}
} }
}, { }, {
key: "getEventPos", key: "getEventPos",
@@ -726,7 +782,7 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
pos.panelRelY = Math.max(event.offsetY / this.height, 0.001); // broadcast to other graph panels that we are hovering pos.panelRelY = Math.max(event.offsetY / this.height, 0.001); // broadcast to other graph panels that we are hovering
appEvents.emit('graph-hover', { appEvents.emit(CoreEvents.graphHover, {
pos: pos, pos: pos,
panel: this.panel panel: this.panel
}); });
@@ -791,22 +847,23 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
}, { }, {
key: "render", key: "render",
value: function render() { value: function render() {
this.data = this.ctrl.data;
this.panel = this.ctrl.panel; this.panel = this.ctrl.panel;
this.timeRange = this.ctrl.range; this.timeRange = this.ctrl.range;
this.cardsData = this.ctrl.cardsData; this.bucketMatrix = this.ctrl.bucketMatrix;
this.bucketMatrixPager = this.ctrl.bucketMatrixPager;
if (!this.data || !this.cardsData || !this.setElementHeight()) { if (!this.bucketMatrix || !this.setElementHeight()) {
return; return;
} // Draw default axes and return if no data } // Draw default axes and return if no data
if (_.isEmpty(this.cardsData.cards)) { this.addStatusmapCanvas();
this.addHeatmapCanvas();
if (this.bucketMatrix.noDatapoints) {
return; return;
} }
this.addHeatmap(); this.addStatusmap();
this.scope.yAxisWidth = this.yAxisWidth; this.scope.yAxisWidth = this.yAxisWidth;
this.scope.xAxisHeight = this.xAxisHeight; this.scope.xAxisHeight = this.xAxisHeight;
this.scope.chartHeight = this.chartHeight; this.scope.chartHeight = this.chartHeight;
@@ -832,7 +889,7 @@ System.register(["lodash", "jquery", "moment", "app/core/utils/kbn", "app/core/c
"id": i, "id": i,
"anno": d.source "anno": d.source
}; };
}); //console.log({"ctrl_annotations": this.ctrl.annotations, "annoData": annoData}); }); //({"ctrl_annotations": this.ctrl.annotations, "annoData": annoData});
var anno = this.heatmap.append("g").attr("class", "statusmap-annotations").attr("transform", "translate(0.5,0)").selectAll(".statusmap-annotations").data(annoData).enter().append("g"); var anno = this.heatmap.append("g").attr("class", "statusmap-annotations").attr("transform", "translate(0.5,0)").selectAll(".statusmap-annotations").data(annoData).enter().append("g");
+1 -1
View File
File diff suppressed because one or more lines are too long
+166
View File
@@ -0,0 +1,166 @@
"use strict";
System.register(["../panel_config_migration"], function (_export, _context) {
"use strict";
var migratePanelConfig;
return {
setters: [function (_panel_config_migration) {
migratePanelConfig = _panel_config_migration.migratePanelConfig;
}],
execute: function () {
describe('when migrate from config with urls', function () {
var panel;
describe('given full panel configuration', function () {
var panelConfig = {
"cards": {
"cardHSpacing": 2,
"cardMinWidth": 5,
"cardRound": null,
"cardVSpacing": 2
},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateGnYlRd",
"defaultColor": "#757575",
"exponent": 0.5,
"mode": "opacity",
"thresholds": []
},
"data": {
"decimals": null,
"unitFormat": "short"
},
"datasource": "TestData DB",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"highlightCards": true,
"id": 4,
"legend": {
"show": false
},
"nullPointMode": "as empty",
"seriesFilterIndex": -1,
"targets": [{
"aggregation": "Last",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1,3,3"
},
"decimals": 2,
"displayAliasType": "Warning / Critical",
"displayType": "Regular",
"displayValueWithAlias": "Never",
"refId": "A",
"scenarioId": "predictable_csv_wave",
"stringInput": "",
"units": "none",
"valueHandler": "Number Threshold"
}],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"show": true
},
"type": "flant-statusmap-panel",
"urls": [{
"base_url": "",
"extraSeries": {
"index": -1
},
"forcelowercase": true,
"icon_fa": "external-link",
"label": "",
"tooltip": "",
"useExtraSeries": false,
"useseriesname": true
}],
"useMax": true,
"usingUrl": true,
"xAxis": {
"labelFormat": "%a %m/%d",
"show": true
},
"yAxis": {
"maxWidth": -1,
"minWidth": -1,
"show": true
},
"yAxisSort": "metrics"
};
beforeEach(function () {
panel = Object.assign({}, panelConfig);
migratePanelConfig(panel);
});
it('should migrate usingUrls to tooltip.freezeOnClick', function () {
expect(panel.usingUrl).toBeUndefined();
expect(panel.tooltip).toHaveProperty("freezeOnClick", true);
expect(panel.tooltip.freezeOnClick).toBeTruthy();
});
it('should have no urls', function () {
expect(panel.urls).toBeUndefined();
});
it('should have empty items in tooltip', function () {
expect(panel.tooltip).toHaveProperty("items", []);
expect(panel.tooltip.items).toHaveLength(0);
});
});
describe("given configuration with non-empty base_url", function () {
describe("with variables", function () {
var panelConfig = {
"tooltip": {
"show": true
},
"urls": [{
"base_url": "https://google.com/$series_label$time",
"extraSeries": {
"index": -1
},
"forcelowercase": true,
"icon_fa": "external-link",
"label": "google",
"tooltip": "",
"useExtraSeries": false,
"useseriesname": true
}, {
"base_url": "example.com/$series_extra",
"extraSeries": {
"format": "YYYY/MM/DD/HH_mm_ss",
"index": -1
},
"forcelowercase": true,
"icon_fa": "external-link",
"label": "DateLink",
"useExtraSeries": true
}]
};
beforeEach(function () {
panel = Object.assign({}, panelConfig);
migratePanelConfig(panel);
});
it("should have equal size of tooltip.items", function () {
expect(panel.tooltip).toHaveProperty("items");
expect(panel.tooltip.items).toHaveLength(panelConfig.urls.length);
});
it("should replace time variable with __url_time_range", function () {
expect(panel.tooltip.items[0].urlTemplate).toContain("${__url_time_range}");
});
it("should replace series_label variable with __y_label", function () {
expect(panel.tooltip.items[0].urlTemplate).toContain("${__y_label}");
});
it("should replace series_extra variable with __value_date", function () {
expect(panel.tooltip.items[1].urlTemplate).toContain("${__value_date}");
});
});
});
});
}
};
});
//# sourceMappingURL=panel_config_migration.js.map
File diff suppressed because one or more lines are too long
+267 -34
View File
@@ -3,69 +3,302 @@
System.register([], function (_export, _context) { System.register([], function (_export, _context) {
"use strict"; "use strict";
var Card, CardsStorage; var Bucket, BucketMatrix, pagerChanged, BucketMatrixPager;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
return { return {
setters: [], setters: [],
execute: function () { execute: function () {
// A holder of values // A holder of a group of values
_export("Card", Card = // uniq _export("Bucket", Bucket =
// Array of values in this bucket /*#__PURE__*/
// card has multiple values function () {
// card has values that has no color // uniq id
// // Array of values in this bucket
// // From pr/86
// // a bucket has multiple values
function Card() { // a bucket has values that has no mapped color
_classCallCheck(this, Card); // y label
// This value can be used to calculate a x coordinate on a graph
// a time range of this bucket
// to and from relative to real "from"
// Saved minimum and maximum of values in this bucket
// A value if multiple values is not allowed
function Bucket() {
_classCallCheck(this, Bucket);
_defineProperty(this, "id", 0); _defineProperty(this, "id", 0);
_defineProperty(this, "values", []); _defineProperty(this, "values", []);
_defineProperty(this, "multipleValues", false); _defineProperty(this, "columns", []);
_defineProperty(this, "noColorDefined", false); _defineProperty(this, "multipleValues", false);
_defineProperty(this, "y", ""); _defineProperty(this, "noColorDefined", false);
_defineProperty(this, "x", 0); _defineProperty(this, "y", "");
_defineProperty(this, "minValue", 0); _defineProperty(this, "yLabel", "");
_defineProperty(this, "maxValue", 0); _defineProperty(this, "x", 0);
_defineProperty(this, "value", 0); _defineProperty(this, "xid", 0);
_defineProperty(this, "from", 0);
_defineProperty(this, "to", 0);
_defineProperty(this, "relFrom", 0);
_defineProperty(this, "relTo", 0);
_defineProperty(this, "mostRecent", false);
_defineProperty(this, "minValue", 0);
_defineProperty(this, "maxValue", 0);
_defineProperty(this, "value", 0);
}
_createClass(Bucket, [{
key: "belong",
value: function belong(ts) {
return ts >= this.from && ts <= this.to;
}
}, {
key: "put",
value: function put(value) {
this.values.push(value);
}
}, {
key: "done",
value: function done() {// calculate min, max, value
}
}, {
key: "isEmpty",
value: function isEmpty() {
return this.values.length == 0;
}
}]);
return Bucket;
}());
_export("BucketMatrix", BucketMatrix =
/*#__PURE__*/
function () {
// buckets for each y label
// a flag that indicate that buckets has stub values
// An array of row labels
// TODO remove: a transition from CardsData
function BucketMatrix() {
_classCallCheck(this, BucketMatrix);
_defineProperty(this, "buckets", {});
_defineProperty(this, "maxValue", 0);
_defineProperty(this, "minValue", 0);
_defineProperty(this, "multipleValues", false);
_defineProperty(this, "noColorDefined", false);
_defineProperty(this, "noDatapoints", false);
_defineProperty(this, "targets", []);
_defineProperty(this, "rangeMs", 0);
_defineProperty(this, "intervalMs", 0);
_defineProperty(this, "xBucketSize", 0);
}
_createClass(BucketMatrix, [{
key: "get",
value: function get(yid, xid) {
if (yid in this.buckets) {
if (xid in this.buckets[yid]) {
return this.buckets[yid][xid];
}
}
return new Bucket();
}
}, {
key: "hasData",
value: function hasData() {
var _this = this;
var hasData = false;
if (this.targets.length > 0) {
this.targets.map(function (target) {
if (_this.buckets[target].length > 0) {
hasData = true;
}
});
}
return hasData;
}
}]);
return BucketMatrix;
}());
_export("pagerChanged", pagerChanged = {
name: 'statusmap-pager-changed'
}); });
_export("CardsStorage", CardsStorage = function CardsStorage() { _export("BucketMatrixPager", BucketMatrixPager =
_classCallCheck(this, CardsStorage); /*#__PURE__*/
function () {
function BucketMatrixPager() {
_classCallCheck(this, BucketMatrixPager);
_defineProperty(this, "cards", []); _defineProperty(this, "bucketMatrix", void 0);
_defineProperty(this, "xBucketSize", 0); _defineProperty(this, "enable", void 0);
_defineProperty(this, "yBucketSize", 0); _defineProperty(this, "defaultPageSize", -1);
_defineProperty(this, "maxValue", void 0); _defineProperty(this, "pageSize", -1);
_defineProperty(this, "minValue", void 0); _defineProperty(this, "currentPage", 0);
_defineProperty(this, "multipleValues", false); var m = new BucketMatrix();
this.bucketMatrix = m;
} // An array of row labels for current page.
_defineProperty(this, "noColorDefined", false);
_defineProperty(this, "targets", []); _createClass(BucketMatrixPager, [{
key: "targets",
value: function targets() {
if (!this.enable) {
return this.bucketMatrix.targets;
}
_defineProperty(this, "targetIndex", void 0); return this.bucketMatrix.targets.slice(this.pageSize * this.currentPage, this.pageSize * (this.currentPage + 1));
}
}, {
key: "buckets",
value: function buckets() {
if (!this.enable) {
return this.bucketMatrix.buckets;
}
this.maxValue = 0; var buckets = {};
this.minValue = 0; var me = this;
}); this.targets().map(function (rowLabel) {
buckets[rowLabel] = me.bucketMatrix.buckets[rowLabel];
});
return buckets;
}
}, {
key: "setEnable",
value: function setEnable(enable) {
this.enable = enable;
}
}, {
key: "setCurrent",
value: function setCurrent(num) {
this.currentPage = num;
}
}, {
key: "setDefaultPageSize",
value: function setDefaultPageSize(num) {
this.defaultPageSize = num;
}
}, {
key: "setPageSize",
value: function setPageSize(num) {
this.pageSize = num;
}
}, {
key: "pages",
value: function pages() {
return Math.ceil(this.totalRows() / this.pageSize);
}
}, {
key: "totalRows",
value: function totalRows() {
return this.bucketMatrix.targets.length;
}
}, {
key: "pageStartRow",
value: function pageStartRow() {
if (!this.enable) {
return 1;
}
return this.pageSize * this.currentPage + 1;
}
}, {
key: "pageEndRow",
value: function pageEndRow() {
if (!this.enable) {
return this.totalRows();
}
var last = this.pageSize * (this.currentPage + 1);
if (last > this.totalRows()) {
return this.totalRows();
}
return last;
}
}, {
key: "hasNext",
value: function hasNext() {
if (!this.enable) {
return false;
}
if ((this.currentPage + 1) * this.pageSize >= this.totalRows()) {
return false;
}
return true; // currentPage >= ctrl.bucketMatrix.targets.length/ctrl.pageSizeViewer - 1 || ctrl.numberOfPages == 0
}
}, {
key: "hasPrev",
value: function hasPrev() {
if (!this.enable) {
return false;
}
return this.currentPage > 0;
}
}, {
key: "switchToNext",
value: function switchToNext() {
if (this.hasNext()) {
this.currentPage = this.currentPage + 1;
}
}
}, {
key: "switchToPrev",
value: function switchToPrev() {
if (this.hasPrev()) {
this.currentPage = this.currentPage - 1;
}
}
}]);
return BucketMatrixPager;
}());
} }
}; };
}); });
+1 -1
View File
File diff suppressed because one or more lines are too long
+217 -43
View File
@@ -3,7 +3,7 @@
System.register(["d3", "jquery", "lodash"], function (_export, _context) { System.register(["d3", "jquery", "lodash"], function (_export, _context) {
"use strict"; "use strict";
var d3, $, _, TOOLTIP_PADDING_X, TOOLTIP_PADDING_Y, StatusmapTooltip; var d3, $, _, TOOLTIP_PADDING_X, TOOLTIP_PADDING_Y, DefaultValueDateFormat, StatusmapTooltip;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
@@ -23,7 +23,10 @@ System.register(["d3", "jquery", "lodash"], function (_export, _context) {
}], }],
execute: function () { execute: function () {
TOOLTIP_PADDING_X = 30; TOOLTIP_PADDING_X = 30;
TOOLTIP_PADDING_Y = 5; TOOLTIP_PADDING_Y = 5; // TODO rename file to tooltip_ctrl.ts
// TODO move DefaultValueDateFormat into tooltip.ts, as it used in several places (migration, partial and tooltip render)
DefaultValueDateFormat = 'YYYY/MM/DD/HH_mm_ss';
_export("StatusmapTooltip", StatusmapTooltip = _export("StatusmapTooltip", StatusmapTooltip =
/*#__PURE__*/ /*#__PURE__*/
@@ -41,17 +44,21 @@ System.register(["d3", "jquery", "lodash"], function (_export, _context) {
_defineProperty(this, "panel", void 0); _defineProperty(this, "panel", void 0);
_defineProperty(this, "heatmapPanel", void 0); _defineProperty(this, "panelElem", void 0);
_defineProperty(this, "mouseOverBucket", void 0); _defineProperty(this, "mouseOverBucket", void 0);
_defineProperty(this, "originalFillColor", void 0); _defineProperty(this, "originalFillColor", void 0);
_defineProperty(this, "tooltipWidth", void 0);
_defineProperty(this, "tooltipFrozen", void 0);
this.scope = scope; this.scope = scope;
this.dashboard = scope.ctrl.dashboard; this.dashboard = scope.ctrl.dashboard;
this.panelCtrl = scope.ctrl; this.panelCtrl = scope.ctrl;
this.panel = scope.ctrl.panel; this.panel = scope.ctrl.panel;
this.heatmapPanel = elem; this.panelElem = elem;
this.mouseOverBucket = false; this.mouseOverBucket = false;
this.originalFillColor = null; this.originalFillColor = null;
elem.on("mouseover", this.onMouseOver.bind(this)); elem.on("mouseover", this.onMouseOver.bind(this));
@@ -67,7 +74,7 @@ System.register(["d3", "jquery", "lodash"], function (_export, _context) {
if (!this.tooltip) { if (!this.tooltip) {
this.add(); this.add();
this.move(e); this.move(e, this.tooltip);
} }
} }
}, { }, {
@@ -82,12 +89,12 @@ System.register(["d3", "jquery", "lodash"], function (_export, _context) {
return; return;
} }
this.move(e); this.move(e, this.tooltip);
} }
}, { }, {
key: "add", key: "add",
value: function add() { value: function add() {
this.tooltip = d3.select("body").append("div").attr("class", "statusmap-tooltip graph-tooltip grafana-tooltip"); this.tooltip = d3.select("body").append("div").attr("class", "graph-tooltip statusmap-tooltip");
} }
}, { }, {
key: "destroy", key: "destroy",
@@ -98,90 +105,225 @@ System.register(["d3", "jquery", "lodash"], function (_export, _context) {
this.tooltip = null; this.tooltip = null;
} }
}, {
key: "removeFrozen",
value: function removeFrozen() {
if (this.tooltipFrozen) {
this.tooltipFrozen.remove();
this.tooltipFrozen = null;
}
}
}, {
key: "showFrozen",
value: function showFrozen(pos) {
this.removeFrozen();
this.tooltipFrozen = d3.select(this.panelElem[0]).append("div").attr("class", "graph-tooltip statusmap-tooltip statusmap-tooltip-frozen");
this.displayTooltip(pos, this.tooltipFrozen, true);
this.moveRelative(pos, this.tooltipFrozen);
}
}, { }, {
key: "show", key: "show",
value: function show(pos) { value: function show(pos) {
if (!this.panel.tooltip.show || !this.tooltip) { if (!this.panel.tooltip.show || !this.tooltip) {
return; return;
} // shared tooltip mode } // TODO support for shared tooltip mode
if (pos.panelRelY) { if (pos.panelRelY) {
return; return;
} }
var cardId = d3.select(pos.target).attr('cardId'); this.displayTooltip(pos, this.tooltip, false);
this.move(pos, this.tooltip);
} // Retrieve bucket and create html content inside tooltips div element.
if (!cardId) { }, {
key: "displayTooltip",
value: function displayTooltip(pos, tooltip, frozen) {
var cardEl = d3.select(pos.target);
var yid = cardEl.attr('yid');
var xid = cardEl.attr('xid');
var bucket = this.panelCtrl.bucketMatrix.get(yid, xid); // TODO string-to-number conversion for xid
if (!bucket || bucket.isEmpty()) {
this.destroy(); this.destroy();
return; return;
} }
var card = this.panelCtrl.cardsData.cards[cardId]; var timestamp = bucket.to;
var yLabel = bucket.yLabel;
var value = bucket.value;
var values = bucket.values; // TODO create option for this formatting.
if (!card) {
this.destroy();
return;
}
var x = card.x;
var y = card.y;
var value = card.value;
var values = card.values;
var tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss'; var tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
var time = this.dashboard.formatDate(+x, tooltipTimeFormat); var time = this.dashboard.formatDate(+timestamp, tooltipTimeFormat); // Close button for the frozen tooltip.
var tooltipHtml = "<div class=\"graph-tooltip-time\">".concat(time, "</div>\n <div class=\"statusmap-histogram\"></div>");
var tooltipClose = "";
if (frozen) {
tooltipClose = "\n<a class=\"pointer pull-right small tooltip-close\">\n <i class=\"fa fa-remove\"></i>\n</a>\n";
}
var tooltipHtml = "<div class=\"graph-tooltip-time\">".concat(time).concat(tooltipClose, "</div>");
if (this.panel.color.mode === 'discrete') { if (this.panel.color.mode === 'discrete') {
var statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values); var statuses;
var statusesHtml = '';
if (statuses.length === 1) { if (this.panel.seriesFilterIndex >= 0) {
statusesHtml = "status:"; statuses = this.panelCtrl.discreteExtraSeries.convertValueToTooltips(value);
} else if (statuses.length > 1) { } else {
statusesHtml = "statuses:"; statuses = this.panelCtrl.discreteExtraSeries.convertValuesToTooltips(values);
} }
tooltipHtml += "\n <div>\n name: <b>".concat(y, "</b> <br>\n ").concat(statusesHtml, "\n <ul>\n ").concat(_.join(_.map(statuses, function (v) { var statusTitle = "status:";
return "<li style=\"background-color: ".concat(v.color, "\" class=\"discrete-item\">").concat(v.tooltip, "</li>");
if (statuses.length > 1) {
statusTitle = "statuses:";
}
tooltipHtml += "\n <div>\n name: <b>".concat(yLabel, "</b>\n <br>\n <span>").concat(statusTitle, "</span>\n <ul>\n ").concat(_.join(_.map(statuses, function (v) {
return "<li style=\"background-color: ".concat(v.color, "; text-align:center\" class=\"discrete-item\">").concat(v.tooltip, "</li>");
}), ""), "\n </ul>\n </div>"); }), ""), "\n </ul>\n </div>");
} else { } else {
if (values.length === 1) { if (values.length === 1) {
tooltipHtml += "<div> \n name: <b>".concat(y, "</b> <br>\n value: <b>").concat(value, "</b> <br>\n </div>"); tooltipHtml += "<div> \n name: <b>".concat(name, "</b> <br>\n value: <b>").concat(value, "</b> <br>\n </div>");
} else { } else {
tooltipHtml += "<div>\n name: <b>".concat(y, "</b> <br>\n values:\n <ul>\n ").concat(_.join(_.map(values, function (v) { tooltipHtml += "<div>\n name: <b>".concat(name, "</b> <br>\n values:\n <ul>\n ").concat(_.join(_.map(values, function (v) {
return "<li>".concat(v, "</li>"); return "<li>".concat(v, "</li>");
}), ""), "\n </ul>\n </div>"); }), ""), "\n </ul>\n </div>");
} }
} // "Ambiguous bucket state: Multiple values!"; }
tooltipHtml += "<div class=\"statusmap-histogram\"></div>";
if (this.panel.tooltip.showItems) {
// Additional information: urls, etc.
// Clone additional items
var items = JSON.parse(JSON.stringify(this.panel.tooltip.items));
var scopedVars = {};
var valueVar;
for (var i = 0; i < bucket.values.length; i++) {
valueVar = "__value_".concat(i);
scopedVars[valueVar] = {
value: bucket.values[i]
};
}
scopedVars["__value"] = {
value: bucket.value
};
scopedVars["__y_label"] = {
value: yLabel
}; // Grafana 7.0 compatible
scopedVars["__url_time_range"] = {
value: this.panelCtrl.retrieveTimeVar()
};
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = items[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var item = _step.value;
if (_.isEmpty(item.urlTemplate)) {
item.link = "#";
} else {
var dateFormat = item.valueDateFormat;
if (dateFormat == '') {
dateFormat = DefaultValueDateFormat;
}
var valueDateVar = void 0;
for (var _i = 0; _i < bucket.values.length; _i++) {
valueDateVar = "__value_".concat(_i, "_date");
scopedVars[valueDateVar] = {
value: this.dashboard.formatDate(+bucket.values[_i], dateFormat)
};
}
scopedVars["__value_date"] = {
value: this.dashboard.formatDate(+bucket.value, dateFormat)
};
item.link = this.panelCtrl.templateSrv.replace(item.urlTemplate, scopedVars); // Force lowercase for link
if (item.urlToLowerCase) {
item.link = item.link.toLowerCase();
}
}
item.label = item.urlText;
if (_.isEmpty(item.label)) {
item.label = _.isEmpty(item.urlTemplate) ? "Empty URL" : _.truncate(item.link);
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator["return"] != null) {
_iterator["return"]();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
tooltipHtml += _.join(_.map(items, function (v) {
return "\n <div>\n <a href=\"".concat(v.link, "\" target=\"_blank\">\n <div class=\"dashlist-item\">\n <p class=\"dashlist-link dashlist-link-dash-db\">\n <span style=\"word-wrap: break-word;\" class=\"dash-title\">").concat(v.label, "</span><span class=\"dashlist-star\">\n <i class=\"fa fa-").concat(v.urlIcon, "\"></i>\n </span></p> </div></a><div>");
}), "\n");
} // Ambiguous state: there multiple values in bucket!
// TODO rename useMax to expectMultipleValues
if (!this.panel.useMax && card.multipleValues) { if (!this.panel.useMax && bucket.multipleValues) {
tooltipHtml += "<div><b>Error:</b> ".concat(this.panelCtrl.dataWarnings.multipleValues.title, "</div>"); tooltipHtml += "<div><b>Error:</b> ".concat(this.panelCtrl.dataWarnings.multipleValues.title, "</div>");
} // Discrete mode errors } // Discrete mode errors
if (this.panel.color.mode === 'discrete') { if (this.panel.color.mode === 'discrete') {
if (card.noColorDefined) { if (bucket.noColorDefined) {
var badValues = this.panelCtrl.discreteHelper.getNotColoredValues(values); var badValues = this.panelCtrl.discreteExtraSeries.getNotColoredValues(values);
tooltipHtml += "<div><b>Error:</b> ".concat(this.panelCtrl.dataWarnings.noColorDefined.title, "\n <br>not colored values:\n <ul>\n ").concat(_.join(_.map(badValues, function (v) { tooltipHtml += "<div><b>Error:</b> ".concat(this.panelCtrl.dataWarnings.noColorDefined.title, "\n <br>not colored values:\n <ul>\n ").concat(_.join(_.map(badValues, function (v) {
return "<li>".concat(v, "</li>"); return "<li>".concat(v, "</li>");
}), ""), "\n </ul>\n </div>"); }), ""), "\n </ul>\n </div>");
} }
} }
this.tooltip.html(tooltipHtml); tooltip.html(tooltipHtml); // Assign mouse event handlers for "frozen" tooltip.
this.move(pos);
} if (frozen) {
// Stop propagation mouse events up to parents to allow interaction with frozen tooltips elements.
tooltip.on("click", function () {
d3.event.stopPropagation();
}).on("mousedown", function () {
d3.event.stopPropagation();
}).on("mouseup", function () {
d3.event.stopPropagation();
}); // Activate close button
tooltip.select("a.tooltip-close").on("click", this.removeFrozen.bind(this));
}
} // Move tooltip as absolute positioned element.
}, { }, {
key: "move", key: "move",
value: function move(pos) { value: function move(pos, tooltip) {
if (!this.tooltip) { if (!tooltip) {
return; return;
} }
var elem = $(this.tooltip.node())[0]; var elem = $(tooltip.node())[0];
var tooltipWidth = elem.clientWidth; var tooltipWidth = elem.clientWidth;
this.tooltipWidth = tooltipWidth;
var tooltipHeight = elem.clientHeight; var tooltipHeight = elem.clientHeight;
var left = pos.pageX + TOOLTIP_PADDING_X; var left = pos.pageX + TOOLTIP_PADDING_X;
var top = pos.pageY + TOOLTIP_PADDING_Y; var top = pos.pageY + TOOLTIP_PADDING_Y;
@@ -194,7 +336,39 @@ System.register(["d3", "jquery", "lodash"], function (_export, _context) {
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y; top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
} }
return this.tooltip.style("left", left + "px").style("top", top + "px"); return tooltip.style("left", left + "px").style("top", top + "px");
} // Move tooltip relative to svg element of panel.
}, {
key: "moveRelative",
value: function moveRelative(pos, tooltip) {
if (!tooltip) {
return;
}
var panelX = pos.pageX - this.panelElem.offset().left;
var panelY = pos.pageY - this.panelElem.offset().top;
var panelWidth = this.panelElem.width();
var panelHeight = this.panelElem.height(); // 'position: relative' sets tooltips width to 100% of panel element.
// Restore width from floating tooltip and add more space for 'Close' button.
var tooltipWidth = this.tooltipWidth + 25; // Left property is clamped so tooltip stays inside panel bound box.
var tooltipLeft = panelX + TOOLTIP_PADDING_X;
if (tooltipLeft + tooltipWidth > panelWidth) {
tooltipLeft = panelWidth - tooltipWidth;
}
if (tooltipLeft < 0) {
tooltipLeft = 0;
} // Frozen tooltips root element is appended next to panels svg element,
// so top property is adjusted to move tooltips root element
// up to the mouse pointer position.
var tooltipTop = -(panelHeight - panelY + TOOLTIP_PADDING_Y);
return tooltip.style("left", tooltipLeft + "px").style("top", tooltipTop + "px").style("width", tooltipWidth + "px");
} }
}]); }]);
+1 -1
View File
File diff suppressed because one or more lines are too long
+95
View File
@@ -0,0 +1,95 @@
"use strict";
System.register([], function (_export, _context) {
"use strict";
var emptyTooltipItem, TooltipEditorCtrl;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function tooltipEditorCtrl() {
'use strict';
return {
restrict: 'E',
scope: true,
templateUrl: 'public/plugins/flant-statusmap-panel/partials/tooltip_editor.html',
controller: TooltipEditorCtrl
};
}
_export("tooltipEditorCtrl", tooltipEditorCtrl);
return {
setters: [],
execute: function () {
emptyTooltipItem = {
urlText: '',
urlTemplate: '',
urlIcon: 'external-link',
urlToLowerCase: true,
valueDateFormat: ''
};
_export("TooltipEditorCtrl", TooltipEditorCtrl =
/*#__PURE__*/
function () {
function TooltipEditorCtrl($scope) {
_classCallCheck(this, TooltipEditorCtrl);
_defineProperty(this, "panel", void 0);
_defineProperty(this, "panelCtrl", void 0);
$scope.editor = this;
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
}
_createClass(TooltipEditorCtrl, [{
key: "render",
value: function render() {
this.panelCtrl.render();
}
}, {
key: "onAddUrl",
value: function onAddUrl() {
this.panel.tooltip.items.push(Object.assign({}, emptyTooltipItem));
this.render();
}
}, {
key: "onRemoveUrl",
value: function onRemoveUrl(index) {
this.panel.tooltip.items.splice(index, 1);
this.render();
}
}, {
key: "onRemoveUrls",
value: function onRemoveUrls() {
this.panel.tooltip.items = [];
this.render();
}
}, {
key: "getFAIconClasses",
value: function getFAIconClasses() {
return ['external-link', 'plus', 'anchor', 'ban', 'globe', 'gear', 'cloud', 'download', 'cloud-download'];
}
}, {
key: "getValueDateFormats",
value: function getValueDateFormats() {
return ['YYYY/MM/DD/HH_mm_ss', 'YYYYMMDDHHmmss', 'YYYY-MM-DD-HH-mm-ss'];
}
}]);
return TooltipEditorCtrl;
}());
}
};
});
//# sourceMappingURL=tooltip_editor.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../src/tooltip_editor.ts"],"names":["tooltipEditorCtrl","restrict","scope","templateUrl","controller","TooltipEditorCtrl","emptyTooltipItem","urlText","urlTemplate","urlIcon","urlToLowerCase","valueDateFormat","$scope","editor","panelCtrl","ctrl","panel","render","tooltip","items","push","Object","assign","index","splice"],"mappings":";;;;;;;;;;;;;;;AAoDO,WAASA,iBAAT,GAA6B;AAClC;;AACA,WAAO;AACLC,MAAAA,QAAQ,EAAE,GADL;AAELC,MAAAA,KAAK,EAAE,IAFF;AAGLC,MAAAA,WAAW,EAAE,mEAHR;AAILC,MAAAA,UAAU,EAAEC;AAJP,KAAP;AAMD;;+BAReL,iB;;;;;AAlDZM,MAAAA,gB,GAAmB;AACrBC,QAAAA,OAAO,EAAE,EADY;AAErBC,QAAAA,WAAW,EAAE,EAFQ;AAGrBC,QAAAA,OAAO,EAAE,eAHY;AAIrBC,QAAAA,cAAc,EAAE,IAJK;AAKrBC,QAAAA,eAAe,EAAE;AALI,O;;mCAQVN,iB;;;AAIX,mCAAYO,MAAZ,EAAyB;AAAA;;AAAA;;AAAA;;AACvBA,UAAAA,MAAM,CAACC,MAAP,GAAgB,IAAhB;AACA,eAAKC,SAAL,GAAiBF,MAAM,CAACG,IAAxB;AACA,eAAKC,KAAL,GAAa,KAAKF,SAAL,CAAeE,KAA5B;AACD;;;;mCAEQ;AACP,iBAAKF,SAAL,CAAeG,MAAf;AACD;;;qCAEU;AACT,iBAAKD,KAAL,CAAWE,OAAX,CAAmBC,KAAnB,CAAyBC,IAAzB,CAA8BC,MAAM,CAACC,MAAP,CAAc,EAAd,EAAkBhB,gBAAlB,CAA9B;AACA,iBAAKW,MAAL;AACD;;;sCAEWM,K,EAAe;AACzB,iBAAKP,KAAL,CAAWE,OAAX,CAAmBC,KAAnB,CAAyBK,MAAzB,CAAgCD,KAAhC,EAAuC,CAAvC;AACA,iBAAKN,MAAL;AACD;;;yCAEc;AACb,iBAAKD,KAAL,CAAWE,OAAX,CAAmBC,KAAnB,GAA2B,EAA3B;AACA,iBAAKF,MAAL;AACD;;;6CAEkB;AACjB,mBAAO,CAAC,eAAD,EAAkB,MAAlB,EAA0B,QAA1B,EAAoC,KAApC,EAA2C,OAA3C,EAAoD,MAApD,EAA4D,OAA5D,EAAqE,UAArE,EAAiF,gBAAjF,CAAP;AACD;;;gDAEqB;AACpB,mBAAO,CACH,qBADG,EAEH,gBAFG,EAGH,qBAHG,CAAP;AAKD","sourcesContent":["import { StatusHeatmapCtrl } from './module';\n\nlet emptyTooltipItem = {\n urlText: '',\n urlTemplate: '',\n urlIcon: 'external-link',\n urlToLowerCase: true,\n valueDateFormat: ''\n};\n\nexport class TooltipEditorCtrl {\n panel: any;\n panelCtrl: StatusHeatmapCtrl;\n\n constructor($scope: any) {\n $scope.editor = this;\n this.panelCtrl = $scope.ctrl as StatusHeatmapCtrl;\n this.panel = this.panelCtrl.panel;\n }\n\n render() {\n this.panelCtrl.render();\n }\n\n onAddUrl() {\n this.panel.tooltip.items.push(Object.assign({}, emptyTooltipItem));\n this.render();\n }\n\n onRemoveUrl(index: number) {\n this.panel.tooltip.items.splice(index, 1);\n this.render();\n }\n\n onRemoveUrls() {\n this.panel.tooltip.items = [];\n this.render();\n }\n\n getFAIconClasses() {\n return ['external-link', 'plus', 'anchor', 'ban', 'globe', 'gear', 'cloud', 'download', 'cloud-download'];\n }\n\n getValueDateFormats() {\n return [\n 'YYYY/MM/DD/HH_mm_ss',\n 'YYYYMMDDHHmmss',\n 'YYYY-MM-DD-HH-mm-ss'\n ];\n }\n}\n\nexport function tooltipEditorCtrl() {\n 'use strict';\n return {\n restrict: 'E',\n scope: true,\n templateUrl: 'public/plugins/flant-statusmap-panel/partials/tooltip_editor.html',\n controller: TooltipEditorCtrl,\n };\n}\n"],"file":"tooltip_editor.js"}
+14 -18
View File
@@ -63,10 +63,6 @@
} }
] ]
}, },
"data": {
"decimals": null,
"unitFormat": "short"
},
"datasource": "prometheus", "datasource": "prometheus",
"gridPos": { "gridPos": {
"h": 16, "h": 16,
@@ -130,21 +126,21 @@
], ],
"title": "Pod Status", "title": "Pod Status",
"tooltip": { "tooltip": {
"show": true "show": true,
"freezeOnClick": false,
"showItems": false,
"items": []
}, },
"transparent": true, "transparent": true,
"type": "flant-statusmap-panel", "type": "flant-statusmap-panel",
"useMax": true, "useMax": true,
"xAxis": { "xAxis": {
"labelFormat": "%a %m/%d", "show": true
"minBucketWidthToShowWeekends": 4,
"show": true,
"showCrosshair": true,
"showWeekends": true
}, },
"yAxis": { "yAxis": {
"show": true, "show": true,
"showCrosshair": false "minWidth": -1,
"maxWidth": -1
}, },
"yAxisSort": "a → z" "yAxisSort": "a → z"
}, },
@@ -310,21 +306,21 @@
], ],
"title": "Container Status", "title": "Container Status",
"tooltip": { "tooltip": {
"show": true "show": true,
"freezeOnClick": true,
"showItems": false,
"items": []
}, },
"transparent": true, "transparent": true,
"type": "flant-statusmap-panel", "type": "flant-statusmap-panel",
"useMax": true, "useMax": true,
"xAxis": { "xAxis": {
"labelFormat": "%a %m/%d", "show": true
"minBucketWidthToShowWeekends": 4,
"show": true,
"showCrosshair": true,
"showWeekends": true
}, },
"yAxis": { "yAxis": {
"show": true, "show": true,
"showCrosshair": false "minWidth": -1,
"maxWidth": -1
}, },
"yAxisSort": "a → z" "yAxisSort": "a → z"
} }
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
verbose: false,
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
moduleDirectories: ['node_modules'],
roots: ['<rootDir>/src'],
testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
//setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'],
//snapshotSerializers: ['enzyme-to-json/serializer'],
globals: { 'ts-jest': { isolatedModules: true } },
};
+2237 -7
View File
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -5,7 +5,8 @@
"main": "README.md", "main": "README.md",
"scripts": { "scripts": {
"build": "grunt", "build": "grunt",
"check-types": "node_modules/typescript/bin/tsc" "check-types": "node_modules/typescript/bin/tsc",
"test": "jest"
}, },
"author": "Flant JSC", "author": "Flant JSC",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -26,6 +27,7 @@
"@types/d3-scale-chromatic": "1.3.1", "@types/d3-scale-chromatic": "1.3.1",
"@types/angular": "1.6.56", "@types/angular": "1.6.56",
"@types/grafana": "github:flant/types-grafana", "@types/grafana": "github:flant/types-grafana",
"@types/jest": "24.0.13",
"@types/jquery": "1.10.35", "@types/jquery": "1.10.35",
"@types/lodash": "4.14.123", "@types/lodash": "4.14.123",
"babel-core": "7.0.0-bridge.0", "babel-core": "7.0.0-bridge.0",
@@ -43,13 +45,18 @@
"grunt-contrib-uglify": "^3.0.1", "grunt-contrib-uglify": "^3.0.1",
"grunt-contrib-watch": "^1.0.0", "grunt-contrib-watch": "^1.0.0",
"grunt-notify": "^0.4.5", "grunt-notify": "^0.4.5",
"jest": "24.8.0",
"jest-canvas-mock": "2.1.2",
"jest-date-mock": "1.0.7",
"load-grunt-tasks": "^3.5.2", "load-grunt-tasks": "^3.5.2",
"typescript": "3.7.2" "typescript": "3.7.2",
"ts-jest": "24.1.0",
"tslib": "1.10.0"
}, },
"dependencies": { "dependencies": {
"d3": "4.13.0", "d3": "4.13.0",
"d3-scale-chromatic": "1.5.0", "d3-scale-chromatic": "1.5.0",
"lodash": "4.17.15", "lodash": "4.17.19",
"moment": "2.24.0" "moment": "2.24.0"
} }
} }
+37 -23
View File
@@ -5,6 +5,8 @@ import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
import {contextSrv} from 'app/core/core'; import {contextSrv} from 'app/core/core';
import {tickStep} from 'app/core/utils/ticks'; import {tickStep} from 'app/core/utils/ticks';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import { StatusHeatmapCtrl } from "./module";
import { PanelEvents } from './libs/grafana/events/index';
const LEGEND_STEP_WIDTH = 2; const LEGEND_STEP_WIDTH = 2;
@@ -15,13 +17,13 @@ coreModule.directive('optionsColorLegend', function() {
return { return {
restrict: 'E', restrict: 'E',
template: '<div class="status-heatmap-color-legend"><svg width="16.8rem" height="24px"></svg></div>', template: '<div class="status-heatmap-color-legend"><svg width="16.8rem" height="24px"></svg></div>',
link: function(scope, elem, attrs) { link: function(scope: any, elem, attrs) {
let ctrl = scope.ctrl; let ctrl = scope.ctrl;
let panel = scope.ctrl.panel; let panel = scope.ctrl.panel;
render(); render();
ctrl.events.on('render', function() { ctrl.events.on(PanelEvents.render, function() {
render(); render();
}); });
@@ -50,25 +52,38 @@ coreModule.directive('statusHeatmapLegend', function() {
restrict: 'E', restrict: 'E',
template: '<div class="status-heatmap-color-legend"><svg width="100px" height="6px"></svg></div>', template: '<div class="status-heatmap-color-legend"><svg width="100px" height="6px"></svg></div>',
link: function(scope, elem, attrs) { link: function(scope, elem, attrs) {
let ctrl = scope.ctrl; let ctrl:StatusHeatmapCtrl = scope.ctrl;
let panel = scope.ctrl.panel; let panel = scope.ctrl.panel;
render(); render();
ctrl.events.on('render', function() { ctrl.events.on(PanelEvents.render, function() {
render(); render();
}); });
function render() { function render() {
clearLegend(elem); clearLegend(elem);
if (!ctrl.panel.legend.show) { if (!ctrl.panel.legend.show) {
return return;
} }
if (!_.isEmpty(ctrl.cardsData) && !_.isEmpty(ctrl.cardsData.cards)) { if (ctrl.bucketMatrix) {
let rangeFrom = ctrl.cardsData.minValue; let rangeFrom = ctrl.bucketMatrix.minValue;
let rangeTo = ctrl.cardsData.maxValue; let rangeTo = ctrl.bucketMatrix.maxValue;
let maxValue = panel.color.max || rangeTo; let maxValue = panel.color.max || rangeTo;
let minValue = panel.color.min || rangeFrom; let minValue = panel.color.min || rangeFrom;
if (ctrl.bucketMatrix.noDatapoints) {
if (!panel.color.max) {
rangeTo = maxValue = 100;
} else {
rangeTo = 100;
}
if (!panel.color.min) {
rangeFrom = minValue = 0;
} else {
rangeFrom = 0;
}
}
if (panel.color.mode === 'spectrum') { if (panel.color.mode === 'spectrum') {
let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme}); let colorScheme = _.find(ctrl.colorSchemes, {value: panel.color.colorScheme});
drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue); drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue);
@@ -77,7 +92,7 @@ coreModule.directive('statusHeatmapLegend', function() {
drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue); drawOpacityLegend(elem, colorOptions, rangeFrom, rangeTo, maxValue, minValue);
} else if (panel.color.mode === 'discrete') { } else if (panel.color.mode === 'discrete') {
let colorOptions = panel.color; let colorOptions = panel.color;
drawDiscreteColorLegend(elem, colorOptions, ctrl.discreteHelper); drawDiscreteColorLegend(elem, colorOptions, ctrl.discreteExtraSeries);
} }
} }
} }
@@ -85,9 +100,9 @@ coreModule.directive('statusHeatmapLegend', function() {
}; };
}); });
function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) { function drawColorLegend(elem, colorScheme, rangeFrom: number, rangeTo:number, maxValue: number, minValue:number) {
let legendElem = $(elem).find('svg'); let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0)); let legend: any = d3.select(legendElem.get(0));
clearLegend(elem); clearLegend(elem);
let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values let legendWidth = Math.floor(legendElem.outerWidth()) - 30; // narrow legendWidth by 30px to get space for first and last tick values
@@ -105,7 +120,7 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal
.enter().append("rect") .enter().append("rect")
// translate from range space into pixels // translate from range space into pixels
// and shift all rectangles to the right by 10 // and shift all rectangles to the right by 10
.attr("x", d => d * widthFactor+10) .attr("x", d => ((d - rangeFrom) * widthFactor)+10)
.attr("y", 0) .attr("y", 0)
// rectangles are slightly overlaped to prevent gaps // rectangles are slightly overlaped to prevent gaps
.attr("width", LEGEND_STEP_WIDTH+1) .attr("width", LEGEND_STEP_WIDTH+1)
@@ -148,7 +163,7 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue
drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth); drawLegendValues(elem, opacityScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth);
} }
function drawDiscreteColorLegend(elem, colorOptions, discreteHelper) { function drawDiscreteColorLegend(elem, colorOptions, discreteExtraSeries) {
let legendElem = $(elem).find('svg'); let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0)); let legend = d3.select(legendElem.get(0));
clearLegend(elem); clearLegend(elem);
@@ -158,7 +173,7 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteHelper) {
let valuesNumber = thresholds.length; let valuesNumber = thresholds.length;
// graph width as a fallback // graph width as a fallback
const $heatmap = $(elem).parent().parent().parent().find('.status-heatmap-panel'); const $heatmap = $(elem).parent().parent().parent().find('.statusmap-panel');
const graphWidthAttr = $heatmap.find('svg').attr("width"); const graphWidthAttr = $heatmap.find('svg').attr("width");
let graphWidth = parseInt(graphWidthAttr); let graphWidth = parseInt(graphWidthAttr);
@@ -196,13 +211,13 @@ function drawDiscreteColorLegend(elem, colorOptions, discreteHelper) {
.attr("width", itemWidth + 1) // Overlap rectangles to prevent gaps .attr("width", itemWidth + 1) // Overlap rectangles to prevent gaps
.attr("height", legendHeight) .attr("height", legendHeight)
.attr("stroke-width", 0) .attr("stroke-width", 0)
.attr("fill", d => discreteHelper.getDiscreteColor(d)); .attr("fill", d => discreteExtraSeries.getDiscreteColor(d));
drawDiscreteLegendValues(elem, colorOptions, legendWidth); drawDiscreteLegendValues(elem, colorOptions, legendWidth);
} }
function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minValue, legendWidth) { function drawLegendValues(elem, colorScale, rangeFrom: number, rangeTo: number, maxValue: number, minValue: number, legendWidth: number) {
let legendElem = $(elem).find('svg'); let legendElem = $(elem).find('svg');
let legend = d3.select(legendElem.get(0)); let legend = d3.select(legendElem.get(0));
@@ -211,10 +226,10 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
} }
let legendValueScale = d3.scaleLinear() let legendValueScale = d3.scaleLinear()
.domain([0, rangeTo]) .domain([rangeFrom, rangeTo])
.range([0, legendWidth]); .range([0, legendWidth]);
let ticks = buildLegendTicks(0, rangeTo, maxValue, minValue); let ticks = buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue);
let xAxis = d3.axisBottom(legendValueScale) let xAxis = d3.axisBottom(legendValueScale)
.tickValues(ticks) .tickValues(ticks)
.tickSize(2); .tickSize(2);
@@ -243,8 +258,7 @@ function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
let valuesNumber = thresholds.length; let valuesNumber = thresholds.length;
let rangeStep = Math.floor(legendWidth / valuesNumber); let rangeStep = Math.floor(legendWidth / valuesNumber);
let valuesRange = d3.range(0, legendWidth, rangeStep); //let valuesRange = d3.range(0, legendWidth, rangeStep);
let legendValueScale = d3.scaleLinear() let legendValueScale = d3.scaleLinear()
.domain([0, valuesNumber]) .domain([0, valuesNumber])
@@ -261,7 +275,7 @@ function drawDiscreteLegendValues(elem, colorOptions, legendWidth) {
.tickValues(d3.range(0, valuesNumber, 1)) //thresholdValues) .tickValues(d3.range(0, valuesNumber, 1)) //thresholdValues)
.tickSize(2) .tickSize(2)
.tickFormat((t) => { .tickFormat((t) => {
let i = Math.floor(t); let i = Math.floor(t.valueOf());
let v = thresholdTooltips[i]; let v = thresholdTooltips[i];
if (v != undefined) { if (v != undefined) {
return ""+v; return ""+v;
@@ -401,7 +415,7 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
let ticks:any = []; let ticks:any = [];
for (let i = 0; i < ticksNum; i++) { for (let i = 0; i < ticksNum; i++) {
let current = tickStepSize * i; let current = tickStepSize * i + rangeFrom;
// Add user-defined min and max if it had been set // Add user-defined min and max if it had been set
if (isValueCloseTo(minValue, current, tickStepSize)) { if (isValueCloseTo(minValue, current, tickStepSize)) {
ticks.push(minValue); ticks.push(minValue);
@@ -415,7 +429,7 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) {
} else if (maxValue < current) { } else if (maxValue < current) {
ticks.push(maxValue); ticks.push(maxValue);
} }
ticks.push(tickStepSize * i); ticks.push(current);
} }
if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) { if (!isValueCloseTo(maxValue, rangeTo, tickStepSize)) {
ticks.push(maxValue); ticks.push(maxValue);
+70 -16
View File
@@ -1,5 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import {StatusHeatmapCtrl} from "./status_heatmap_ctrl"; import { Bucket } from "./statusmap_data";
import { StatusHeatmapCtrl } from "./module";
interface Tooltip { interface Tooltip {
tooltip: string; tooltip: string;
@@ -12,7 +13,7 @@ declare class DiscreteColorThreshold {
tooltip: string; tooltip: string;
} }
// Helper methods to handle discrete color mode // Extra Series methods to handle discrete color mode
export class ColorModeDiscrete { export class ColorModeDiscrete {
scope: any; scope: any;
panelCtrl: StatusHeatmapCtrl; panelCtrl: StatusHeatmapCtrl;
@@ -42,6 +43,23 @@ export class ColorModeDiscrete {
return tooltips; return tooltips;
} }
convertValueToTooltips(values) {
let thresholds = this.panel.color.thresholds;
let tooltips = [];
for (let i = 0; i < thresholds.length; i++) {
//for (let j = 0; j < values.length; j++) {
if (values == thresholds[i].value) {
tooltips.push({
"tooltip": thresholds[i].tooltip?thresholds[i].tooltip:values,
"color": thresholds[i].color
});
//}
}
}
return tooltips;
}
getNotMatchedValues(values:any[]) { getNotMatchedValues(values:any[]) {
let notMatched:any[] = []; let notMatched:any[] = [];
for (let j = 0; j < values.length; j++) { for (let j = 0; j < values.length; j++) {
@@ -71,6 +89,22 @@ export class ColorModeDiscrete {
return color; return color;
} }
getBucketColorSingle(value) {
//let thresholds = this.panel.color.thresholds;
if (value == null) {
// treat as null value
return 'rgba(0,0,0,1)';
//return this.getMatchedThreshold(null).color;
}
let threshold = this.getMatchedThreshold(value);
if (!threshold || !threshold.color || threshold.color == "") {
return 'rgba(0,0,0,1)';
} else {
return threshold.color;
}
}
// returns color from first matched thresold in order from 0 to thresholds.length // returns color from first matched thresold in order from 0 to thresholds.length
getBucketColor(values) { getBucketColor(values) {
let thresholds = this.panel.color.thresholds; let thresholds = this.panel.color.thresholds;
@@ -109,24 +143,44 @@ export class ColorModeDiscrete {
return 'rgba(0,0,0,1)'; return 'rgba(0,0,0,1)';
} }
updateCardsValuesHasColorInfoSingle() {
if (!this.panelCtrl.bucketMatrix) {
return;
}
this.panelCtrl.bucketMatrix.noColorDefined = false;
this.panelCtrl.bucketMatrix.targets.map((target:string) => {
this.panelCtrl.bucketMatrix.buckets[target].map((bucket:Bucket) => {
bucket.noColorDefined = false;
let threshold = this.getMatchedThreshold(bucket.value);
if (!threshold || !threshold.color || threshold.color == "") {
bucket.noColorDefined = true;
this.panelCtrl.bucketMatrix.noColorDefined = true;
}
});
});
}
updateCardsValuesHasColorInfo() { updateCardsValuesHasColorInfo() {
if (!this.panelCtrl.cardsData) { if (!this.panelCtrl.bucketMatrix) {
return return
} }
this.panelCtrl.cardsData.noColorDefined = false; this.panelCtrl.bucketMatrix.noColorDefined = false;
let cards = this.panelCtrl.cardsData.cards;
for (let i=0; i<cards.length; i++) { this.panelCtrl.bucketMatrix.targets.map((target:string) => {
cards[i].noColorDefined = false; this.panelCtrl.bucketMatrix.buckets[target].map((bucket:Bucket) => {
let values = cards[i].values; bucket.noColorDefined = false;
for (let j=0; j<values.length; j++) { for (let j=0; j<bucket.values.length; j++) {
let threshold = this.getMatchedThreshold(values[j]); let threshold = this.getMatchedThreshold(bucket.values[j]);
if (!threshold || !threshold.color || threshold.color == "") { if (!threshold || !threshold.color || threshold.color == "") {
cards[i].noColorDefined = true; bucket.noColorDefined = true;
this.panelCtrl.cardsData.noColorDefined = true; this.panelCtrl.bucketMatrix.noColorDefined = true;
break; break;
}
} }
} });
} });
} }
getMatchedThreshold(value) { getMatchedThreshold(value) {
+49 -3
View File
@@ -14,7 +14,7 @@
} }
} }
.status-heatmap-panel { .statusmap-panel {
position: relative; position: relative;
.axis .tick { .axis .tick {
@@ -41,19 +41,37 @@
} }
.statusmap-tooltip { .statusmap-tooltip {
// from .grafana-tooltip
// '.grafana-tooltip a' makes links not readable for tooltip items.
position: absolute;
padding: 10px;
font-weight: 200;
border-radius: 5px;
max-width: 800px;
max-height: 600px;
overflow: hidden;
line-height: 14px;
z-index: 9999;
// more overrides
white-space: nowrap; white-space: nowrap;
font-size: $font-size-sm; font-size: $font-size-sm;
background-color: $graph-tooltip-bg; background-color: $graph-tooltip-bg;
color: $text-color; color: $text-color;
.discrete-item { .discrete-item {
color: #52545c; color: #ffffff;
padding: 1px; padding: 1px;
font-weight: bold; font-weight: bold;
text-shadow: 0 0 0.2em #FFF, 0 0 0.2em #FFF, 0 0 0.2em #FFF; text-shadow: 0 0 0.2em #212124, 0 0 0.2em #212124, 0 0 0.2em #212124;
} }
} }
.statusmap-tooltip-frozen {
position: relative;
z-index: 1;
}
.statusmap-histogram rect { .statusmap-histogram rect {
fill: $text-color-weak; fill: $text-color-weak;
} }
@@ -71,6 +89,34 @@
stroke: rgba(102, 102, 102, 0.8); stroke: rgba(102, 102, 102, 0.8);
} }
.width-c-40 {
width: 40.0rem!important
}
.width-c-50 {
width: 50.0rem!important
}
.width-c-60 {
width: 60.0rem!important
}
.width-c-70 {
width: 70.0rem!important
}
.statusmap-pagination {
// space between legend and pagination controls
margin-top: 5px;
label {
margin-right:0px;
}
input {
margin-right: 0px;
}
.last-in-group {
margin-right: 4px;
}
}
.pagination-buttons{text-align: right;}
.status-heatmap-legend-wrapper { .status-heatmap-legend-wrapper {
margin: 0 10px; margin: 0 10px;
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+4
View File
@@ -0,0 +1,4 @@
export interface AppEvent<T> {
readonly name: string;
payload?: T;
}
+16
View File
@@ -0,0 +1,16 @@
import {AppEvent} from './appEvents';
export interface GraphHoverPayload {
pos: any;
panel: {
id: number;
};
}
export var graphHover:(AppEvent<GraphHoverPayload>|string) = {name: 'graph-hover'};
export var graphHoverClear:(AppEvent<any>|string) = {name: 'graph-hover-clear'};
export function fallbackToStringEvents() {
graphHover = 'graph-hover';
graphHoverClear = 'graph-hover-clear';
}
+3
View File
@@ -0,0 +1,3 @@
import * as CoreEvents from './events';
import * as PanelEvents from './panelEvents';
export { CoreEvents, PanelEvents }
+29
View File
@@ -0,0 +1,29 @@
import { AppEvent } from './appEvents';
export interface DataQueryError {
data?: {
message?: string;
error?: string;
};
message?: string;
status?: string;
statusText?: string;
refId?: string;
cancelled?: boolean;
}
export var refresh:(AppEvent<undefined>|string) = {name: 'refresh'};
export var render:(AppEvent<any>|string) = {name: 'render'};
export var dataError:(AppEvent<DataQueryError>|string) = {name: 'data-error'};
export var dataReceived:(AppEvent<any[]>|string) = {name: 'data-received'};
export var dataSnapshotLoad:(AppEvent<any[]>|string) = {name: 'data-snapshot-load'};
export var editModeInitialized:(AppEvent<undefined>|string) = {name: 'init-edit-mode'};
export function fallbackToStringEvents() {
refresh = 'refresh';
render = 'render';
dataError = 'data-error';
dataReceived = 'data-received';
dataSnapshotLoad = 'data-snapshot-load';
editModeInitialized = 'init-edit-mode';
}
+8
View File
@@ -0,0 +1,8 @@
1. @grafana/data is not supported in grafana 5.x and 6.2. The workaround is
the module `./libs/grafana/events` that include copy-pasted events
from @grafana/data and app/core from grafana sources.
2. Newly Grafana versions flood Console with
"Usage strings as events is deprecated ..." messages.
This module has a function to detect if Grafanas Emitter support AppEvent style event ids.
+23
View File
@@ -0,0 +1,23 @@
import { Emitter } from 'app/core/utils/emitter';
// Old Grafana releases use strings as event ids and
// new event ids in form of object {name: "event-id"} are not
// supported properly: first defined listener will receive
// all emitted events despite of the value in 'name' field.
//
// This method detects this behaviour and return true
// only for new Grafana versions.
export function hasAppEventCompatibleEmitter(emitter: Emitter):boolean {
let receiveEvents = 0;
let eventId: any = {name: "non-existed-event-id"};
let eventId2: any = {name: "non-existed-event-id-2"};
emitter.on(eventId, function(){
receiveEvents++;
});
emitter.emit(eventId);
emitter.emit(eventId2);
emitter.removeAllListeners(eventId);
// New Grafana versions should receive one event.
return receiveEvents == 1;
}
+2
View File
@@ -0,0 +1,2 @@
import * as Polygrafill from './funcs';
export { Polygrafill };
+26 -4
View File
@@ -1,15 +1,37 @@
<div class="status-heatmap-wrapper"> <div class="status-heatmap-wrapper">
<div class="status-heatmap-canvas-wrapper"> <div class="status-heatmap-canvas-wrapper">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined || ctrl.noDatapoints">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined">
<span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span> <span class="small" ng-if="ctrl.multipleValues" bs-tooltip="'{{ctrl.dataWarnings.multipleValues.tip}}'">{{ctrl.dataWarnings.multipleValues.title}}</span>
<span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span> <span class="small" ng-if="ctrl.noColorDefined" bs-tooltip="'{{ctrl.dataWarnings.noColorDefined.tip}}'">{{ctrl.dataWarnings.noColorDefined.title}}</span>
<span class="small" ng-if="ctrl.noDatapoints" bs-tooltip="'{{ctrl.dataWarnings.noDatapoints.tip}}'">{{ctrl.dataWarnings.noDatapoints.title}}</span>
</div> </div>
<div class="statusmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
<div class="status-heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
</div> </div>
<div class="status-heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show"> <div class="status-heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show">
<status-heatmap-legend></status-heatmap-legend> <status-heatmap-legend></status-heatmap-legend>
</div> </div>
<div class="clearfix"></div>
<div class="statusmap-pagination gf-form" ng-if="ctrl.panel.usingPagination">
<div class="gf-form">
<button class="btn btn-inverse" ng-disabled="!pager.hasPrev()" ng-click="ctrl.onPrevPage()">
<i class="fa fa-arrow-left"></i>&nbsp;Previous
</button>
<label class="gf-form-label">{{pager.currentPage+1}}/{{pager.pages()}}</label>
<button class="btn btn-inverse last-in-group" ng-disabled="!pager.hasNext()" ng-click="ctrl.onNextPage()">
Next&nbsp;<i class="fa fa-arrow-right"></i>
</button>
</div>
<div class="gf-form">
<label class="gf-form-label">Rows per page</label>
<input type="number" class="gf-form-input width-3 last-in-group" placeholder="12" data-placement="top"
bs-tooltip="'Number of rows to show per page.'" ng-model="ctrl.pageSizeViewer" ng-change="ctrl.onChangePageSize()"
ng-model-onblur>
</div>
<label class="gf-form-label">Show {{pager.pageStartRow()}} to {{pager.pageEndRow()}} of {{pager.totalRows()}} rows</label>
</div>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
+402 -196
View File
@@ -4,6 +4,9 @@ import { auto } from 'angular';
// Components // Components
import './color_legend'; import './color_legend';
import { optionsEditorCtrl } from './options_editor';
import { tooltipEditorCtrl } from './tooltip_editor';
import { migratePanelConfig } from './panel_config_migration';
// Utils // Utils
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
@@ -12,21 +15,17 @@ import {loadPluginCss} from 'app/plugins/sdk';
// Types // Types
import { MetricsPanelCtrl } from 'app/plugins/sdk'; import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { AnnotationsSrv } from 'app/features/annotations/annotations_srv'; import { AnnotationsSrv } from 'app/features/annotations/annotations_srv';
import {CardsStorage, Card} from './statusmap_data'; import { CoreEvents, PanelEvents } from './libs/grafana/events/index';
import {Bucket, BucketMatrix, BucketMatrixPager } from './statusmap_data';
import rendering from './rendering'; import rendering from './rendering';
// import aggregates, { aggregatesMap } from './aggregates'; import { Polygrafill } from './libs/polygrafill/index';
// import fragments, { fragmentsMap } from './fragments';
// import { labelFormats } from './xAxisLabelFormats';
import {statusHeatmapOptionsEditor} from './options_editor';
import {ColorModeDiscrete} from "./color_mode_discrete"; import {ColorModeDiscrete} from "./color_mode_discrete";
const CANVAS = 'CANVAS';
const SVG = 'SVG';
const VALUE_INDEX = 0, const VALUE_INDEX = 0,
TIME_INDEX = 1; TIME_INDEX = 1;
const renderer = CANVAS;
const colorSchemes = [ const colorSchemes = [
// Diverging // Diverging
{ name: 'Spectral', value: 'interpolateSpectral', invert: 'always' }, { name: 'Spectral', value: 'interpolateSpectral', invert: 'always' },
@@ -59,46 +58,40 @@ const colorSchemes = [
let colorModes = ['opacity', 'spectrum', 'discrete']; let colorModes = ['opacity', 'spectrum', 'discrete'];
let opacityScales = ['linear', 'sqrt']; let opacityScales = ['linear', 'sqrt'];
interface DataWarning {
title: string;
tip: string;
}
interface DataWarnings {
noColorDefined: DataWarning;
multipleValues: DataWarning;
}
interface ColorThreshold {
}
loadPluginCss({ loadPluginCss({
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css', dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css' light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
}); });
export var renderComplete:any = {name:'statusmap-render-complete'};
class StatusHeatmapCtrl extends MetricsPanelCtrl { class StatusHeatmapCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html'; static templateUrl = 'module.html';
data: any;
bucketMatrix: BucketMatrix;
bucketMatrixPager: BucketMatrixPager;
graph: any;
discreteHelper: ColorModeDiscrete;
opacityScales: any = []; opacityScales: any = [];
colorModes: any = []; colorModes: any = [];
colorSchemes: any = []; colorSchemes: any = [];
unitFormats: any; unitFormats: any;
data: any;
cardsData: any; dataWarnings: {[warningId: string]: {title: string, tip: string}} = {};
graph: any;
multipleValues: boolean; multipleValues: boolean;
noColorDefined: boolean; noColorDefined: boolean;
discreteHelper: ColorModeDiscrete; noDatapoints: boolean;
dataWarnings: DataWarnings;
discreteExtraSeries: ColorModeDiscrete;
annotations: object[] = []; annotations: object[] = [];
annotationsPromise: any; annotationsPromise: any;
// TODO remove this transient variable: use ng-model-options="{ getterSetter: true }"
pageSizeViewer: number = 15;
panelDefaults: any = { panelDefaults: any = {
// datasource name, null = default datasource // datasource name, null = default datasource
datasource: null, datasource: null,
@@ -121,39 +114,57 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
cardRound: null cardRound: null
}, },
xAxis: { xAxis: {
show: true, show: true
showWeekends: true,
minBucketWidthToShowWeekends: 4,
showCrosshair: true,
labelFormat: '%a %m/%d'
}, },
yAxis: { yAxis: {
show: true, show: true,
showCrosshair: false minWidth: -1,
maxWidth: -1
}, },
tooltip: { tooltip: {
show: true show: true,
freezeOnClick: true,
showItems: false,
items: [] // see tooltip_editor.ts
}, },
legend: { legend: {
show: true show: true
}, },
data: {
unitFormat: 'short',
decimals: null
},
// how null points should be handled // how null points should be handled
nullPointMode: 'as empty', nullPointMode: 'as empty',
yAxisSort: 'metrics', yAxisSort: 'metrics',
highlightCards: true, highlightCards: true,
useMax: true useMax: true,
seriesFilterIndex: -1,
// Pagination options
usingPagination: false,
pageSize: 15
}; };
/** @ngInject */ /** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService, private annotationsSrv: AnnotationsSrv) { constructor($scope: any, $injector: auto.IInjectorService, private annotationsSrv: AnnotationsSrv) {
super($scope, $injector); super($scope, $injector);
if(!Polygrafill.hasAppEventCompatibleEmitter(this.events)){
CoreEvents.fallbackToStringEvents();
PanelEvents.fallbackToStringEvents();
renderComplete = 'statusmap-render-complete';
}
migratePanelConfig(this.panel);
_.defaultsDeep(this.panel, this.panelDefaults); _.defaultsDeep(this.panel, this.panelDefaults);
this.bucketMatrix = new BucketMatrix();
// Create pager for bucketMatrix and restore page size.
this.bucketMatrixPager = new BucketMatrixPager();
this.bucketMatrixPager.setEnable(this.panel.usingPagination);
this.bucketMatrixPager.setDefaultPageSize(this.panel.pageSize);
this.bucketMatrixPager.setPageSize(this.panel.pageSize);
$scope.pager = this.bucketMatrixPager;
this.opacityScales = opacityScales; this.opacityScales = opacityScales;
this.colorModes = colorModes; this.colorModes = colorModes;
this.colorSchemes = colorSchemes; this.colorSchemes = colorSchemes;
@@ -166,42 +177,89 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.multipleValues = false; this.multipleValues = false;
this.noColorDefined = false; this.noColorDefined = false;
this.discreteHelper = new ColorModeDiscrete($scope); this.discreteExtraSeries = new ColorModeDiscrete($scope);
this.dataWarnings = { this.dataWarnings = {
"noColorDefined": { noColorDefined: {
title: 'Data has value with undefined color', title: 'Data has value with undefined color',
tip: 'Check metric values, color values or define a new color', tip: 'Check metric values, color values or define a new color',
}, },
"multipleValues": { multipleValues: {
title: 'Data has multiple values for one target', title: 'Data has multiple values for one target',
tip: 'Change targets definitions or set "use max value"', tip: 'Change targets definitions or set "use max value"',
},
noDatapoints: {
title: 'No data points',
tip: 'No datapoints returned from data query',
} }
}; };
this.annotations = []; this.annotations = [];
this.annotationsSrv = annotationsSrv;
this.events.on('render', this.onRender.bind(this)); this.events.on(PanelEvents.render, this.onRender.bind(this));
this.events.on('data-received', this.onDataReceived.bind(this)); this.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this));
this.events.on('data-error', this.onDataError.bind(this)); this.events.on(PanelEvents.dataError, this.onDataError.bind(this));
this.events.on('data-snapshot-load', this.onDataReceived.bind(this)); this.events.on(PanelEvents.dataSnapshotLoad, this.onDataReceived.bind(this));
this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));
this.events.on('refresh', this.postRefresh.bind(this)); this.events.on(PanelEvents.refresh, this.postRefresh.bind(this));
// custom event from rendering.js // custom event from rendering.js
this.events.on('render-complete', this.onRenderComplete.bind(this)); this.events.on(renderComplete, this.onRenderComplete.bind(this));
this.onCardColorChange = this.onCardColorChange.bind(this);
} }
onRenderComplete(data):void { onRenderComplete(data: any):void {
this.graph.chartWidth = data.chartWidth; this.graph.chartWidth = data.chartWidth;
this.renderingCompleted(); this.renderingCompleted();
} }
changeDefaultPaginationSize(defaultPageSize: number): void {
this.bucketMatrixPager.setDefaultPageSize(defaultPageSize);
this.bucketMatrixPager.setPageSize(defaultPageSize);
this.pageSizeViewer = defaultPageSize;
this.render();
this.refresh();
}
onChangePageSize(): void {
if (this.pageSizeViewer <= 0) {
this.pageSizeViewer = this.bucketMatrixPager.defaultPageSize;
}
this.bucketMatrixPager.setPageSize(this.pageSizeViewer);
this.bucketMatrixPager.setCurrent(0);
this.render();
this.refresh();
}
onPrevPage(): void {
this.bucketMatrixPager.switchToPrev();
this.render();
}
onNextPage(): void {
this.bucketMatrixPager.switchToNext();
this.render();
}
// getChartWidth returns an approximation of chart canvas width or
// a saved value calculated during a render.
getChartWidth():number { getChartWidth():number {
if (this.graph.chartWidth > 0) {
return this.graph.chartWidth;
}
const wndWidth = $(window).width(); const wndWidth = $(window).width();
// gripPos.w is a width in grid's measurements. Grid size in Grafana is 24. // gripPos.w is a width in grid's measurements. Grid size in Grafana is 24.
const panelWidthFactor = this.panel.gridPos.w / 24; const panelWidthFactor = this.panel.gridPos.w / 24;
const panelWidth = Math.ceil(wndWidth * panelWidthFactor); const panelWidth = Math.ceil(wndWidth * panelWidthFactor);
// approximate chartWidth because y axis ticks not rendered yet on first data receive. // approximate width of the chart draw canvas:
// - y axis ticks are not rendered yet on first data receive,
// so choose 200 as a decent value for y legend width
// - chartWidth can not be lower than the half of the panel width.
const chartWidth = _.max([ const chartWidth = _.max([
panelWidth - 200, panelWidth - 200,
panelWidth/2 panelWidth/2
@@ -210,7 +268,18 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
return chartWidth!; return chartWidth!;
} }
// override calculateInterval for discrete color mode // Quick workaround for 6.7 and 7.0+. There is no call to
// calculateInterval in updateTimeRange in those versions.
// TODO ts type has no argument for this method.
updateTimeRange(datasource?: any) {
let ret = super.updateTimeRange(datasource);
this.calculateInterval();
return ret;
}
// calculateInterval is called on 'refresh' to calculate an interval
// for datasource.
// It is override of calculateInterval from MetricsPanelCtrl.
calculateInterval() { calculateInterval() {
let chartWidth = this.getChartWidth(); let chartWidth = this.getChartWidth();
@@ -222,7 +291,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
let rangeMs = this.range.to.valueOf() - this.range.from.valueOf(); let rangeMs = this.range.to.valueOf() - this.range.from.valueOf();
// this is minimal interval! kbn.round_interval will lower it. // this is minimal interval! kbn.round_interval will lower it.
intervalMs = this.discreteHelper.roundIntervalCeil(rangeMs / maxCardsCount); intervalMs = this.discreteExtraSeries.roundIntervalCeil(rangeMs / maxCardsCount);
// Calculate low limit of interval // Calculate low limit of interval
let lowLimitMs = 1; // 1 millisecond default low limit let lowLimitMs = 1; // 1 millisecond default low limit
@@ -250,6 +319,10 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.intervalMs = intervalMs; this.intervalMs = intervalMs;
this.interval = interval; this.interval = interval;
// Get final buckets count after interval is adjusted
// TODO is it needed?
//this.xBucketsCount = Math.floor(rangeMs / intervalMs);
} }
issueQueries(datasource: any) { issueQueries(datasource: any) {
@@ -265,8 +338,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
* (but not wait for completion). This resolves * (but not wait for completion). This resolves
* issue 11806. * issue 11806.
*/ */
// 5.x before 5.4 doesn't have datasourcePromises. // 5.x before 5.4 doesn't have datasourcePromises.
if ("undefined" !== typeof(this.annotationsSrv.datasourcePromises)) { if ("undefined" !== typeof(this.annotationsSrv.datasourcePromises)) {
return this.annotationsSrv.datasourcePromises.then(r => { return this.annotationsSrv.datasourcePromises.then(r => {
return this.issueQueriesWithInterval(datasource, this.interval); return this.issueQueriesWithInterval(datasource, this.interval);
@@ -281,63 +353,91 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
// so we need to save-restore this.panel.interval. // so we need to save-restore this.panel.interval.
issueQueriesWithInterval(datasource: any, interval: any) { issueQueriesWithInterval(datasource: any, interval: any) {
var origInterval = this.panel.interval; var origInterval = this.panel.interval;
this.panel.interval = this.interval; this.panel.interval = interval;
var res = super.issueQueries(datasource); var res = super.issueQueries(datasource);
this.panel.interval = origInterval; this.panel.interval = origInterval;
return res; return res;
} }
onDataReceived(dataList: any) { onDataReceived(dataList: any) {
this.data = dataList; this.data = dataList;
this.cardsData = this.convertToCards(this.data); // Quick workaround for 7.0+. There is no call to
// calculateInterval when enter Edit mode.
if (!this.intervalMs) {
this.calculateInterval();
}
console.log("OnDataReceived"); let newBucketMatrix = this.convertDataToBuckets(dataList, this.range.from.valueOf(), this.range.to.valueOf(), this.intervalMs, true);
this.annotationsPromise.then( this.bucketMatrix = newBucketMatrix;
(result: { alertState: any; annotations: any }) => { this.bucketMatrixPager.bucketMatrix = newBucketMatrix;
this.loading = false; if (newBucketMatrix.targets.length !== this.bucketMatrix.targets.length) {
//this.alertState = result.alertState; this.bucketMatrixPager.setCurrent(0);
if (result.annotations && result.annotations.length > 0) { }
this.annotations = result.annotations;
} else { this.noDatapoints = this.bucketMatrix.noDatapoints;
if (this.annotationsPromise) {
this.annotationsPromise.then(
(result: { alertState: any; annotations: any }) => {
this.loading = false;
//this.alertState = result.alertState;
if (result.annotations && result.annotations.length > 0) {
this.annotations = result.annotations;
} else {
this.annotations = [];
}
this.render();
},
() => {
this.loading = false;
this.annotations = []; this.annotations = [];
this.render();
} }
console.log("annotationsPromise result " + this.annotations.length + " annotations"); );
this.render(); } else {
}, this.loading = false;
() => { this.annotations = [];
this.loading = false; this.render();
this.annotations = []; }
console.log("annotationsPromise onrejected");
this.render();
}
);
//this.render();
} }
onInitEditMode() { onInitEditMode() {
this.addEditorTab('Options', statusHeatmapOptionsEditor, 2); this.addEditorTab('Options', optionsEditorCtrl, 2);
this.addEditorTab('Tooltip', tooltipEditorCtrl, 3);
this.unitFormats = kbn.getUnitFormats(); this.unitFormats = kbn.getUnitFormats();
} }
// onRender will be called before StatusmapRenderer.onRender.
// Decide if warning should be displayed over cards.
onRender() { onRender() {
if (!this.range || !this.data) { return; } if (!this.range || !this.data) {
this.noDatapoints = true;
return;
}
this.multipleValues = false; this.multipleValues = false;
if (!this.panel.useMax) { if (!this.panel.useMax) {
if (this.cardsData) { if (this.bucketMatrix) {
this.multipleValues = this.cardsData.multipleValues; this.multipleValues = this.bucketMatrix.multipleValues;
} }
} }
this.noColorDefined = false; this.noColorDefined = false;
if (this.panel.color.mode === 'discrete') { if (this.panel.color.mode === 'discrete') {
this.discreteHelper.updateCardsValuesHasColorInfo(); if (this.panel.seriesFilterIndex == -1) {
if (this.cardsData) { this.discreteExtraSeries.updateCardsValuesHasColorInfo();
this.noColorDefined = this.cardsData.noColorDefined; } else {
this.discreteExtraSeries.updateCardsValuesHasColorInfoSingle();
} }
if (this.bucketMatrix) {
this.noColorDefined = this.bucketMatrix.noColorDefined;
}
}
this.noDatapoints = false;
if (this.bucketMatrix) {
this.noDatapoints = this.bucketMatrix.noDatapoints;
} }
} }
@@ -356,121 +456,227 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.noColorDefined = false; this.noColorDefined = false;
} }
onEditorAddThreshold() {
this.panel.color.thresholds.push({ color: this.panel.defaultColor });
this.render();
}
onEditorRemoveThreshold(index:number) {
this.panel.color.thresholds.splice(index, 1);
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" });
this.panel.color.thresholds.push({color: "green", value: 0, tooltip: "ok" });
this.render();
}
/* https://ethanschoonover.com/solarized/ */
onEditorAddSolarized() {
this.panel.color.thresholds.push({color: "#b58900", value: 0, tooltip: "yellow" });
this.panel.color.thresholds.push({color: "#cb4b16", value: 1, tooltip: "orange" });
this.panel.color.thresholds.push({color: "#dc322f", value: 2, tooltip: "red" });
this.panel.color.thresholds.push({color: "#d33682", value: 3, tooltip: "magenta" });
this.panel.color.thresholds.push({color: "#6c71c4", value: 4, tooltip: "violet" });
this.panel.color.thresholds.push({color: "#268bd2", value: 5, tooltip: "blue" });
this.panel.color.thresholds.push({color: "#2aa198", value: 6, tooltip: "cyan" });
this.panel.color.thresholds.push({color: "#859900", value: 7, tooltip: "green" });
this.render();
}
link(scope, elem, attrs, ctrl) { link(scope, elem, attrs, ctrl) {
rendering(scope, elem, attrs, ctrl); rendering(scope, elem, attrs, ctrl);
} }
// group values into buckets by target // Compatible with Grafana 7.0 overrides feature.
convertToCards(data) { retrieveTimeVar() {
let cardsData = <CardsStorage> { var time = this.timeSrv.timeRangeForUrl();
cards: [], return 'from=' + time.from + '&to=' + time.to;
xBucketSize: 0, }
yBucketSize: 0,
maxValue: 0,
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;} // convertToBuckets groups values in data into buckets by target and timestamp.
//
// data is a result from datasource. It is an array of timeseries and tables:
/* [
// timeseries
{
target: "query alias",
refId: "A",
datapoints: [
[0, 1582681239911],
[3, 158....],
...
],
tags?:{key: value,...}
},
// table
{
name: "table name",
refId: "B",
columns: [
{text: "id"},
{text: "info"},
...
],
rows: [
[1, "123"],
[2, "44411"],
...
]
},
...
]
// Collect uniq timestamps from data and spread over targets and timestamps to and from a time range of the panel.
intervalMs a calculated interval. It is used to split a time range.
*/
convertDataToBuckets(data:any, from:number, to:number, intervalMs: number, mostRecentBucket: boolean):BucketMatrix {
let bucketMatrix = new BucketMatrix();
bucketMatrix.rangeMs = to - from;
bucketMatrix.intervalMs = intervalMs;
// collect uniq targets and their indices if (!data || data.length == 0) {
_.map(data, (d, i) => { // Mimic heatmap and graph 'no data' labels.
cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i) bucketMatrix.targets = [
}); "1.0", "0.0", "-1.0"
];
// TODO add some logic for targets heirarchy bucketMatrix.buckets["1.0"] = [];
cardsData.targets = _.keys(cardsData.targetIndex); bucketMatrix.buckets["0.0"] = [];
cardsData.yBucketSize = cardsData.targets.length; bucketMatrix.buckets["-1.0"] = [];
// Maximum number of buckets over x axis bucketMatrix.xBucketSize = 42;
cardsData.xBucketSize = _.max(_.map(data, d => d.datapoints.length)); bucketMatrix.noDatapoints = true;
return bucketMatrix;
// 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];
for (let j = 0; j < cardsData.xBucketSize; j++) {
let card = new Card();
card.id = i*cardsData.xBucketSize + j;
card.values = [];
card.y = target;
card.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.values.push(datapoint[VALUE_INDEX]);
}
card.minValue = _.min(card.values);
card.maxValue = _.max(card.values);
if (card.values.length > 1) {
cardsData.multipleValues = true;
card.multipleValues = true;
card.value = card.maxValue; // max value by default
} else {
card.value = card.maxValue; // max value by default
}
if (cardsData.maxValue < card.maxValue)
cardsData.maxValue = card.maxValue;
if (cardsData.minValue > card.minValue)
cardsData.minValue = card.minValue;
if (card.x != -1) {
cardsData.cards.push(card);
}
}
} }
return cardsData; let targetIndex: {[target: string]: number[]} = {};
// Group indicies of elements in data by target (y label).
// lodash version:
//_.map(data, (d, i) => {
// targetIndex[d.target] = _.concat(_.toArray(targetIndex[d.target]), i);
//});
data.map((queryResult: any, i: number) => {
let yLabel = queryResult.target;
if (!targetIndex.hasOwnProperty(yLabel)) {
targetIndex[yLabel] = [];
}
targetIndex[yLabel].push(i);
});
let targetKeys = _.keys(targetIndex);
//console.log ("targetIndex: ", targetIndex, "targetKeys: ", targetKeys);
let targetTimestampRanges: {[target: string]: {[timestamp: number]: number[]}} = {};
// Collect all timestamps for each target.
// Make map timestamp => [from, to]. from == previous ts, to == ts from datapoint.
targetKeys.map((target) => {
let targetTimestamps: any[] = [];
for (let si = 0; si < targetIndex[target].length; si++) {
let s = data[targetIndex[target][si]];
_.map(s.datapoints, (datapoint, idx) => {
targetTimestamps.push(datapoint[TIME_INDEX]-from);
})
}
//console.log("timestamps['"+target+"'] = ", targetTimestamps);
targetTimestamps = _.uniq(targetTimestamps);
//console.log("uniq timestamps['"+target+"'] = ", targetTimestamps);
targetTimestampRanges[target] = [];
for (let i = targetTimestamps.length-1 ; i>=0; i-- ) {
let tsTo = targetTimestamps[i];
let tsFrom = 0;
if (tsTo < 0) {
tsFrom = tsTo - intervalMs;
} else {
if (i-1 >= 0) {
// Set from to previous timestamp + 1ms;
tsFrom = targetTimestamps[i-1]+1;
// tfTo - tfFrom should not be more than intervalMs
let minFrom = tsTo - intervalMs;
if (tsFrom < minFrom) {
tsFrom = minFrom;
}
}
}
targetTimestampRanges[target][tsTo] = [tsFrom, tsTo];
}
});
// console.log ("targetTimestampRanges: ", targetTimestampRanges);
// Create empty buckets using intervalMs to calculate ranges.
// If mostRecentBucket is set, create a bucket with a range "to":"to"
// to store most recent values.
targetKeys.map((target) => {
let targetEmptyBuckets: any[] = [];
let lastTs = to-from;
if (mostRecentBucket) {
let topBucket = new Bucket();
topBucket.yLabel = target;
topBucket.relTo = lastTs;
topBucket.relFrom = lastTs;
topBucket.values = [];
topBucket.mostRecent = true;
if (targetTimestampRanges[target].hasOwnProperty(lastTs)) {
topBucket.relFrom = targetTimestampRanges[target][lastTs][0];
lastTs = topBucket.relFrom;
}
topBucket.to = topBucket.relTo+from;
topBucket.from = topBucket.relFrom+from;
targetEmptyBuckets.push(topBucket);
}
let idx = 0;
let bucketFrom: number = 0;
while (bucketFrom >= 0) {
let b = new Bucket();
b.yLabel = target;
b.relTo = lastTs - idx*intervalMs;
b.relFrom = lastTs - ((idx+1) * intervalMs);
b.to = b.relTo+from;
b.from = b.relFrom+from;
b.values = [];
bucketFrom = b.relFrom;
targetEmptyBuckets.push(b);
idx++;
}
targetEmptyBuckets.map((bucket, i) => {
bucket.xid = i;
});
bucketMatrix.buckets[target] = targetEmptyBuckets;
});
//console.log ("bucketMatrix: ", bucketMatrix);
// Put values into buckets.
bucketMatrix.minValue = Number.MAX_VALUE;
bucketMatrix.maxValue = Number.MIN_SAFE_INTEGER;
targetKeys.map((target) => {
targetIndex[target].map((dataIndex) => {
let s = data[dataIndex];
s.datapoints.map((dp: any) => {
for (let i = 0; i < bucketMatrix.buckets[target].length; i++) {
if (bucketMatrix.buckets[target][i].belong(dp[TIME_INDEX])) {
bucketMatrix.buckets[target][i].put(dp[VALUE_INDEX]);
}
}
});
});
bucketMatrix.buckets[target].map((bucket) => {
bucket.minValue = _.min(bucket.values);
bucket.maxValue = _.max(bucket.values);
if (bucket.minValue < bucketMatrix.minValue) {
bucketMatrix.minValue = bucket.minValue;
}
if (bucket.maxValue > bucketMatrix.maxValue) {
bucketMatrix.maxValue = bucket.maxValue;
}
bucket.value = bucket.maxValue;
if (bucket.values.length > 1) {
bucketMatrix.multipleValues = true;
bucket.multipleValues = true;
bucket.value = this.panel.seriesFilterIndex != -1 ? bucket.values[this.panel.seriesFilterIndex] : bucket.maxValue;
}
})
});
bucketMatrix.xBucketSize = Number.MIN_SAFE_INTEGER;
targetKeys.map((target) => {
let bucketsLen: number = bucketMatrix.buckets[target].length;
if (bucketsLen > bucketMatrix.xBucketSize) {
bucketMatrix.xBucketSize = bucketsLen;
}
});
//console.log ("bucketMatrix with values: ", bucketMatrix);
bucketMatrix.targets = targetKeys;
return bucketMatrix;
} }
} }
+48 -6
View File
@@ -1,26 +1,68 @@
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import { StatusHeatmapCtrl } from './module';
export class StatusHeatmapOptionsEditorCtrl { export class StatusHeatmapOptionsEditorCtrl {
panel: any; panel: any;
panelCtrl: any; panelCtrl: StatusHeatmapCtrl;
unitFormats: any; unitFormats: any;
constructor($scope) { /** @ngInject */
constructor($scope: any) {
$scope.editor = this; $scope.editor = this;
this.panelCtrl = $scope.ctrl; this.panelCtrl = $scope.ctrl as StatusHeatmapCtrl;
this.panel = this.panelCtrl.panel; this.panel = this.panelCtrl.panel;
this.unitFormats = kbn.getUnitFormats();
this.panelCtrl.render(); this.unitFormats = kbn.getUnitFormats();
} }
setUnitFormat(subItem) { setUnitFormat(subItem) {
this.panel.data.unitFormat = subItem.value; this.panel.data.unitFormat = subItem.value;
this.panelCtrl.render(); this.panelCtrl.render();
} }
render() {
this.panelCtrl.render();
}
onAddThreshold() {
this.panel.color.thresholds.push({ color: this.panel.defaultColor });
this.render();
}
onRemoveThreshold(index:number) {
this.panel.color.thresholds.splice(index, 1);
this.render();
}
onRemoveThresholds() {
this.panel.color.thresholds = [];
this.render();
}
onAddThreeLights() {
this.panel.color.thresholds.push({color: "red", value: 2, tooltip: "error" });
this.panel.color.thresholds.push({color: "yellow", value: 1, tooltip: "warning" });
this.panel.color.thresholds.push({color: "green", value: 0, tooltip: "ok" });
this.render();
}
/* https://ethanschoonover.com/solarized/ */
onAddSolarized() {
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();
}
} }
export function statusHeatmapOptionsEditor() { /** @ngInject */
export function optionsEditorCtrl() {
'use strict'; 'use strict';
return { return {
restrict: 'E', restrict: 'E',
+115
View File
@@ -0,0 +1,115 @@
import _ from 'lodash';
function migrate_V0_V1(panel: any) {
// Remove unused fields.
if (_.has(panel, "xAxis.labelFormat")) {
delete panel.xAxis.labelFormat;
}
if (_.has(panel, "xAxis.minBucketWidthToShowWeekends")) {
delete panel.xAxis.minBucketWidthToShowWeekends;
}
if (_.has(panel, "xAxis.showCrosshair")) {
delete panel.xAxis.showCrosshair;
}
if (_.has(panel, "xAxis.showWeekends")) {
delete panel.xAxis.showWeekends;
}
if (_.has(panel, "yAxis.showCrosshair")) {
delete panel.yAxis.showCrosshair;
}
if (_.has(panel, "data.unitFormat")) {
delete panel.data;
}
// Migrate cardSpacing value. Seems rare (update from version 0.0.2).
if (_.has(panel, "cards.cardSpacing")) {
if (!_.has(panel, "cards.cardVSpacing")) {
if (panel.cards.cardSpacing) {
panel.cards.cardVSpacing = panel.cards.cardSpacing
panel.cards.cardHSpacing = panel.cards.cardSpacing
}
}
delete panel.cards.cardSpacing;
}
// Migrate initial config for urls in tooltip (pull/86).
// 'usingUrl' was used to show tooltip with urls on click or not.
if (_.has(panel, "usingUrl")) {
if (!_.has(panel, "tooltip.freezeOnClick")) {
panel.tooltip.freezeOnClick = panel.usingUrl;
}
delete panel.usingUrl;
}
// 'urls' array is now tooltip.items array. Also items are changed.
if (_.has(panel, "urls")) {
if (!_.has(panel, "tooltip.items")) {
panel.tooltip.items = [];
let hasRealItems = true;
if (panel.urls.length == 0) {
hasRealItems = false;
}
if (panel.urls.length == 1) {
let url = panel.urls[0];
if (url.base_url === '' && url.label === '') {
hasRealItems = false;
}
}
if (hasRealItems) {
panel.tooltip.showItems = true;
for (let url of panel.urls) {
let item = {
urlTemplate: _.toString(url.base_url),
urlText: _.toString(url.label),
urlIcon: _.toString(url.icon_fa),
urlToLowerCase: url.forcelowercase,
valueDateFormat: '',
}
// replace $vars with new ${__vars} if url template is not empty
if (item.urlTemplate !== "") {
// $time was a graph time with prepended &
item.urlTemplate = _.replace(url.base_url, /\$time/g, "&${__url_time_range}");
// $series_label was a y axis label
item.urlTemplate = _.replace(item.urlTemplate, /\$series_label/, "${__y_label}");
// $series_extra was a value from bucket. This value has format options and index.
let valueVar = "__value";
if (url.useExtraSeries === true) {
// index?
if (url.extraSeries.index > -1) {
valueVar += "_" + url.extraSeries.index;
}
let format = _.toString(url.extraSeries.format);
if (format === 'YYYY/MM/DD/HH_mm_ss') {
valueVar += '_date';
item.valueDateFormat = format;
}
}
item.urlTemplate = _.replace(item.urlTemplate, /\$series_extra/, `\${${valueVar}}`);
}
panel.tooltip.items.push(item);
}
}
}
delete panel.urls;
}
// create statusmap metadata
panel.statusmap = {
"ConfigVersion": "v1",
}
}
export function migratePanelConfig(panel: any) {
if (_.has(panel, "statusmap")) {
if (panel.statusmap.ConfigVersion == "v1") {
return;
}
} else {
migrate_V0_V1(panel);
}
return;
}
+50 -21
View File
@@ -81,9 +81,7 @@
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label"> <label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)"> <i class="fa fa-trash pointer" ng-click="editor.onRemoveThreshold($index)"></i>
<i class="fa fa-trash"/>
</a>
</label> </label>
</div> </div>
</div> </div>
@@ -91,12 +89,12 @@
<div class="gf-form-inline"> <div class="gf-form-inline">
<div class="gf-form"></div> <div class="gf-form"></div>
<div class="gf-form"> <div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorAddThreshold()"> <button class="btn btn-inverse" ng-click="editor.onAddThreshold()">
<i class="fa fa-plus"></i> Add new status <i class="fa fa-plus"></i> Add new status
</button> </button>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorRemoveThresholds()"> <button class="btn btn-inverse" ng-click="editor.onRemoveThresholds()">
<i class="fa fa-minus"></i> Remove all <i class="fa fa-minus"></i> Remove all
</button> </button>
</div> </div>
@@ -108,12 +106,12 @@
<label class="gf-form-label">Presets</label> <label class="gf-form-label">Presets</label>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddThreeLights()"> <button class="btn" ng-click="editor.onAddThreeLights()">
Red-Yellow-Green Red-Yellow-Green
</button> </button>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddSolarized()"> <button class="btn" ng-click="editor.onAddSolarized()">
Solarized Solarized
</button> </button>
</div> </div>
@@ -145,11 +143,24 @@
checked="ctrl.panel.legend.show" on-change="ctrl.render()"> checked="ctrl.panel.legend.show" on-change="ctrl.render()">
</gf-form-switch> </gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-8" <gf-form-switch class="gf-form" label-class="width-8"
label="Show tooltip" label="Show X axis"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()"> checked="ctrl.panel.xAxis.show" on-change="ctrl.render()">
</gf-form-switch> </gf-form-switch>
<div class="gf-form"> <gf-form-switch class="gf-form" label-class="width-8"
<label class="gf-form-label width-8">Y axis sort</label> label="Show Y axis"
checked="ctrl.panel.yAxis.show" on-change="ctrl.render()">
</gf-form-switch>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Min width</label>
<input type="number" ng-model="ctrl.panel.yAxis.minWidth" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.render()" ng-model-onblur>
</div>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Max width</label>
<input type="number" ng-model="ctrl.panel.yAxis.maxWidth" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.render()" ng-model-onblur>
</div>
<div class="gf-form" ng-show="ctrl.panel.yAxis.show">
<label class="gf-form-label width-8">Rows sort</label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input max-width-8" <select class="gf-form-input max-width-8"
ng-model="ctrl.panel.yAxisSort" ng-model="ctrl.panel.yAxisSort"
@@ -157,46 +168,64 @@
ng-change="ctrl.render()"></select> ng-change="ctrl.render()"></select>
</div> </div>
</div> </div>
</div>
</div>
<div class="section gf-form-group"> <div class="section gf-form-group">
<h5 class="section-heading">Bucket</h5> <h5 class="section-heading">Buckets</h5>
<gf-form-switch class="gf-form" label-class="width-9" <gf-form-switch class="gf-form" label-class="width-8"
label="Multiple values" label="Multiple values"
checked="ctrl.panel.useMax" on-change="ctrl.render()"> checked="ctrl.panel.useMax" on-change="ctrl.render()">
</gf-form-switch> </gf-form-switch>
<div class="gf-form" ng-show="ctrl.panel.useMax"> <div class="gf-form" ng-show="ctrl.panel.useMax">
Color for bucket with multiple values is determined by color mode<br/> Color for bucket with multiple values is determined by color mode<br/>
</div> </div>
<div class="gf-form">
<label class="gf-form-label width-8">Values index</label>
<input type="number" class="gf-form-input width-5" placeholder="-1" data-placement="right"
bs-tooltip="'Display only values with this index in a bucket. Use -1 to display all values'"
ng-model="ctrl.panel.seriesFilterIndex" ng-change="ctrl.refresh()"
ng-model-onblur>
</div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">Display nulls</label> <label class="gf-form-label width-8">Display nulls</label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input max-width-9" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['as empty', 'as zero']" ng-change="ctrl.render()"></select> <select class="gf-form-input max-width-9" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['as empty', 'as zero']" ng-change="ctrl.render()"></select>
</div> </div>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">Min width for 1/1</label> <label class="gf-form-label width-8">Min width</label>
<input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur> <input type="number" class="gf-form-input width-5" placeholder="5" data-placement="right" bs-tooltip="'Minimal card width for 1/1 resolution in pixels'" ng-model="ctrl.panel.cards.cardMinWidth" ng-change="ctrl.refresh()" ng-model-onblur>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">V spacing</label> <label class="gf-form-label width-8">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> <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>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">H spacing</label> <label class="gf-form-label width-8">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> <input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right" bs-tooltip="'Cards horizontal spacing in pixels'" ng-model="ctrl.panel.cards.cardHSpacing" ng-change="ctrl.refresh()" ng-model-onblur>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-9">Rounding</label> <label class="gf-form-label width-8">Rounding</label>
<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> <input type="number" class="gf-form-input width-5" placeholder="0" data-placement="right" bs-tooltip="'Cards rounding radius in pixels'" ng-model="ctrl.panel.cards.cardRound" ng-change="ctrl.refresh()" ng-model-onblur>
</div> </div>
</div> </div>
<div class="section gf-form-group"> <div class="section gf-form-group">
<h5 class="section-heading">Pagination</h5>
<div class="gf-form">
<gf-form-switch class="gf-form" label="Enable pagination" label-class="width-12"
checked="ctrl.panel.usingPagination" on-change="ctrl.render()"></gf-form-switch>
</div>
<div class="gf-form" ng-if="ctrl.panel.usingPagination">
<label class="gf-form-label width-10">Rows per page</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="top"
bs-tooltip="'Number of rows to show by default'" ng-model="ctrl.panel.pageSize" ng-change="ctrl.changeDefaultPaginationSize(ctrl.panel.pageSize)"
ng-model-onblur>
</div>
</div> </div>
<div class="section gf-form-group">
</div>
</div> </div>
+119
View File
@@ -0,0 +1,119 @@
<div class="editor-row">
<div class="section gf-form-group">
<gf-form-switch class="gf-form" label-class="width-10"
label="Show tooltip"
tooltip="'Show tooltip when mouse is over the card'"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-10"
label="Freeze on click"
tooltip="'Freeze tooltip copy when card is clicked'"
checked="ctrl.panel.tooltip.freezeOnClick" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-10"
label="Show items"
tooltip="'Show items (urls) in tooltip'"
checked="ctrl.panel.tooltip.showItems" on-change="ctrl.render()">
</gf-form-switch>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Items</h5>
<div class="gf-form" ng-show="ctrl.panel.tooltip.items.length == 0">
<label class="gf-form-label width-2">0</label>
<label class="gf-form-input width-30">No items defined.</label>
</div>
</div>
</div>
<div>
<div ng-repeat="item in ctrl.panel.tooltip.items">
<div class="gf-form gf-form--grow">
<label class="gf-form-label width-6">
URL
<info-popover mode="left-normal">
<p>Specify an URL (relative or absolute)</p>
<span>
Use special variables:
<br>
<em>${__y_label}</em> name of the time series (alias or legend in the query editor).
<br>
<em>${__url_time_range}</em> refers to graph range in the form of 'from=...&to=...'.
<br>
<em>${__value}</em> refers to current bucket value or max value if multiple values mode is on.
<br>
<em>${__value_N}</em> refers to Nth value in current bucket. Value indexes are started from 0. For instance,
<em>${__value_1}</em> refers to second value.
<br>
<em>${__value_date}</em> refers to __value formatted as date.
<em>${__value_N_date}</em> refers to Nth value in current bucket formatted as date.
</span>
</info-popover>
</label>
<input type="text" class="gf-form-input" placeholder="https://www.google.es" data-placement="top"
bs-tooltip="'This is the url to be shown when the user clicks on it'" ng-model="item.urlTemplate"
ng-change="ctrl.refresh()">
<label class="gf-form-label">
<i class="fa fa-trash pointer" ng-click="editor.onRemoveUrl($index)"></i>
</label>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-4">Icon</label>
<label class="gf-form-label width-2">
<i class="fa fa-{{item.urlIcon}}"></i>
</label>
<input
class="gf-form-input width-10"
type="text"
ng-model="item.urlIcon"
bs-typeahead="editor.getFAIconClasses"
bs-tooltip="'The icon for URL link'"
data-placement="top"
ng-blur="ctrl.render()"
data-min-length="0"
data-items="100"
/>
</div>
<div class="gf-form">
<label class="gf-form-label width-4">Label</label>
<input type="text" class="gf-form-input width-16" placeholder="My URL" data-placement="top"
bs-tooltip="'Text for URL link'"
ng-model="item.urlText" ng-change="ctrl.refresh()">
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label-class="width-8" label="Lowercase link" checked="item.urlToLowerCase"
on-change="ctrl.render()">
</gf-form-switch>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Date format</label>
<input type="text" class="gf-form-input width-12" placeholder="YYYY/MM/DD/HH_mm_ss"
data-placement="top" bs-tooltip="'Date format for __value_date'"
bs-typeahead="editor.getValueDateFormats"
data-min-length="0"
data-items="100"
ng-model="item.valueDateFormat"
ng-blur="ctrl.render()">
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form"></div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="editor.onAddUrl()">
<i class="fa fa-plus"></i> Add URL
</button>
</div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="editor.onRemoveUrls()">
<i class="fa fa-minus"></i> Remove all
</button>
</div>
</div>
</div>
+5 -5
View File
@@ -9,7 +9,7 @@
"name": "Flant JSC", "name": "Flant JSC",
"url": "http://flant.com" "url": "http://flant.com"
}, },
"keywords": ["status", "heatmap", "panel"], "keywords": ["status", "statusmap", "heatmap", "panel"],
"logos": { "logos": {
"small": "img/logo.svg", "small": "img/logo.svg",
"large": "img/logo.svg" "large": "img/logo.svg"
@@ -17,7 +17,6 @@
"links": [ "links": [
{"name": "Github", "url": "https://github.com/flant/grafana-statusmap"}, {"name": "Github", "url": "https://github.com/flant/grafana-statusmap"},
{"name": "MIT Licence", "url": "https://github.com/flant/grafana-statusmap/blob/master/LICENSE"} {"name": "MIT Licence", "url": "https://github.com/flant/grafana-statusmap/blob/master/LICENSE"}
], ],
"screenshots": [ "screenshots": [
{"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"}, {"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"},
@@ -25,12 +24,13 @@
{"name": "Color mapping", "path": "img/color-mapping.png"}, {"name": "Color mapping", "path": "img/color-mapping.png"},
{"name": "Queries example", "path": "img/queries-example.png"} {"name": "Queries example", "path": "img/queries-example.png"}
], ],
"version": "0.2.0", "version": "0.3.2",
"updated": "2019-12-03" "updated": "2020-08-13"
}, },
"dependencies": { "dependencies": {
"grafanaVersion": "5.1.x", "grafanaDependency": ">=5.4.3",
"grafanaVersion": "5.4.x",
"plugins": [ ] "plugins": [ ]
} }
} }
+133 -82
View File
@@ -3,11 +3,13 @@ import $ from 'jquery';
import moment from 'moment'; import moment from 'moment';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import {appEvents, contextSrv} from 'app/core/core'; import {appEvents, contextSrv} from 'app/core/core';
import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
import * as d3 from 'd3'; import * as d3 from 'd3';
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index'; import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
import {StatusmapTooltip} from './tooltip'; import {StatusmapTooltip} from './tooltip';
import {AnnotationTooltip} from './annotations'; import {AnnotationTooltip} from './annotations';
import { Bucket, BucketMatrix, BucketMatrixPager } from './statusmap_data';
import { StatusHeatmapCtrl, renderComplete } from './module';
import { CoreEvents, PanelEvents } from './libs/grafana/events/index';
let MIN_CARD_SIZE = 5, let MIN_CARD_SIZE = 5,
CARD_H_SPACING = 2, CARD_H_SPACING = 2,
@@ -24,6 +26,18 @@ export default function rendering(scope: any, elem: any, attrs: any, ctrl: any)
return new StatusmapRenderer(scope, elem, attrs, ctrl); return new StatusmapRenderer(scope, elem, attrs, ctrl);
} }
class Statusmap {
$svg: any;
svg: any;
bucketMatrix: BucketMatrix;
timeRange: {from: number, to: number} = {from:0, to:0};
constructor() {
}
}
export class StatusmapRenderer { export class StatusmapRenderer {
width: number = 0; width: number = 0;
height: number = 0; height: number = 0;
@@ -45,8 +59,9 @@ export class StatusmapRenderer {
mouseUpHandler: any; mouseUpHandler: any;
xGridSize: number = 0; xGridSize: number = 0;
yGridSize: number = 0; yGridSize: number = 0;
data: any;
cardsData: any; bucketMatrix: BucketMatrix;
bucketMatrixPager: BucketMatrixPager;
panel: any; panel: any;
$heatmap: any; $heatmap: any;
tooltip: StatusmapTooltip; tooltip: StatusmapTooltip;
@@ -60,9 +75,9 @@ export class StatusmapRenderer {
margin: any; margin: any;
dataRangeWidingFactor: number = DATA_RANGE_WIDING_FACTOR; dataRangeWidingFactor: number = DATA_RANGE_WIDING_FACTOR;
constructor(private scope: any, private elem: any, attrs: any, private ctrl: any) { constructor(private scope: any, private elem: any, attrs: any, private ctrl: StatusHeatmapCtrl) {
// $heatmap is JQuery object, but heatmap is D3 // $heatmap is JQuery object, but heatmap is D3
this.$heatmap = this.elem.find('.status-heatmap-panel'); this.$heatmap = this.elem.find('.statusmap-panel');
this.tooltip = new StatusmapTooltip(this.$heatmap, this.scope); this.tooltip = new StatusmapTooltip(this.$heatmap, this.scope);
this.annotationTooltip = new AnnotationTooltip(this.$heatmap, this.scope); this.annotationTooltip = new AnnotationTooltip(this.$heatmap, this.scope);
@@ -77,7 +92,7 @@ export class StatusmapRenderer {
this.padding = { left: 0, right: 0, top: 0, bottom: 0 }; this.padding = { left: 0, right: 0, top: 0, bottom: 0 };
this.margin = { left: 25, right: 15, top: 10, bottom: 20 }; this.margin = { left: 25, right: 15, top: 10, bottom: 20 };
this.ctrl.events.on('render', this.onRender.bind(this)); this.ctrl.events.on(PanelEvents.render, this.onRender.bind(this));
this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this); this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this);
@@ -85,15 +100,17 @@ export class StatusmapRenderer {
// Selection and crosshair // // Selection and crosshair //
///////////////////////////// /////////////////////////////
// Shared crosshair and tooltip // Shared crosshair and tooltip this.empty = true;
appEvents.on('graph-hover', this.onGraphHover.bind(this), this.scope);
appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), this.scope); appEvents.on( CoreEvents.graphHover, this.onGraphHover.bind(this), this.scope);
appEvents.on( CoreEvents.graphHoverClear, this.onGraphHoverClear.bind(this), this.scope);
// Register selection listeners // Register selection listeners
this.$heatmap.on('mousedown', this.onMouseDown.bind(this)); this.$heatmap.on('mousedown', this.onMouseDown.bind(this));
this.$heatmap.on('mousemove', this.onMouseMove.bind(this)); this.$heatmap.on('mousemove', this.onMouseMove.bind(this));
this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this)); this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this));
this.$heatmap.on('click', this.onMouseClick.bind(this));
} }
onGraphHoverClear() { onGraphHoverClear() {
@@ -110,14 +127,21 @@ export class StatusmapRenderer {
} }
setElementHeight() { setElementHeight(): boolean {
try { try {
var height = this.ctrl.height || this.panel.height || this.ctrl.row.height; var height = this.ctrl.height || this.panel.height || this.ctrl.row.height;
if (_.isString(height)) { if (_.isString(height)) {
height = parseInt(height.replace('px', ''), 10); height = parseInt(height.replace('px', ''), 10);
} }
height -= this.panel.legend.show ? 32 : 10; // bottom padding and space for legend. Change margin in .status-heatmap-color-legend ! if (this.panel.usingPagination) {
// TODO get height of pagination controls.
// reserve height for legend and for a row of pagination controls.
height -= this.panel.legend.show ? 70 : 40; // bottom padding and space for legend. Change margin in .status-heatmap-color-legend !
} else {
// reserve height for legend
height -= this.panel.legend.show ? 32 : 4; // bottom padding and space for legend. Change margin in .status-heatmap-color-legend !
}
this.$heatmap.css('height', height + 'px'); this.$heatmap.css('height', height + 'px');
@@ -127,7 +151,7 @@ export class StatusmapRenderer {
} }
} }
getYAxisWidth(elem: any) { getYAxisWidth(elem: any): number {
const axisText = elem.selectAll(".axis-y text").nodes(); const axisText = elem.selectAll(".axis-y text").nodes();
const maxTextWidth = _.max(_.map(axisText, text => { const maxTextWidth = _.max(_.map(axisText, text => {
// Use SVG getBBox method // Use SVG getBBox method
@@ -137,7 +161,7 @@ export class StatusmapRenderer {
return Math.ceil(maxTextWidth); return Math.ceil(maxTextWidth);
} }
getXAxisHeight(elem: any) { getXAxisHeight(elem: any): number {
let axisLine = elem.select(".axis-x line"); let axisLine = elem.select(".axis-x line");
if (!axisLine.empty()) { if (!axisLine.empty()) {
let axisLinePosition = parseFloat(elem.select(".axis-x line").attr("y2")); let axisLinePosition = parseFloat(elem.select(".axis-x line").attr("y2"));
@@ -151,6 +175,10 @@ export class StatusmapRenderer {
addXAxis() { addXAxis() {
// Scale timestamps to cards centers // Scale timestamps to cards centers
//this.scope.xScale = this.xScale = d3.scaleTime()
// .domain([this.timeRange.from, this.timeRange.to])
// .range([this.xGridSize/2, this.chartWidth-this.xGridSize/2]);
// Buckets without the most recent
this.scope.xScale = this.xScale = d3.scaleTime() this.scope.xScale = this.xScale = d3.scaleTime()
.domain([this.timeRange.from, this.timeRange.to]) .domain([this.timeRange.from, this.timeRange.to])
.range([this.xGridSize/2, this.chartWidth-this.xGridSize/2]); .range([this.xGridSize/2, this.chartWidth-this.xGridSize/2]);
@@ -188,7 +216,7 @@ export class StatusmapRenderer {
} }
// divide chart height by ticks for cards drawing // divide chart height by ticks for cards drawing
getYScale(ticks) { getYScale(ticks: any[]) {
let range:any[] = []; let range:any[] = [];
let step = this.chartHeight / ticks.length; let step = this.chartHeight / ticks.length;
// svg has y=0 on the top, so top card should have a minimal value in range // svg has y=0 on the top, so top card should have a minimal value in range
@@ -202,7 +230,7 @@ export class StatusmapRenderer {
} }
// divide chart height by ticks with offset for ticks drawing // divide chart height by ticks with offset for ticks drawing
getYAxisScale(ticks) { getYAxisScale(ticks: any[]) {
let range:any[] = []; let range:any[] = [];
let step = this.chartHeight / ticks.length; let step = this.chartHeight / ticks.length;
// svg has y=0 on the top, so top tick should have a minimal value in range // svg has y=0 on the top, so top tick should have a minimal value in range
@@ -216,13 +244,9 @@ export class StatusmapRenderer {
} }
addYAxis() { addYAxis() {
let ticks = _.uniq(_.map(this.data, d => d.target)); let ticks = this.bucketMatrixPager.targets();
// Set default Y min and max if no data
if (_.isEmpty(this.data)) {
ticks = [''];
}
// TODO move sorting into bucketMatrixPager.
if (this.panel.yAxisSort == 'a → z') { if (this.panel.yAxisSort == 'a → z') {
ticks.sort((a, b) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true})); ticks.sort((a, b) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
} else if (this.panel.yAxisSort == 'z → a') { } else if (this.panel.yAxisSort == 'z → a') {
@@ -254,7 +278,7 @@ export class StatusmapRenderer {
} }
// Wide Y values range and adjust to bucket size // Wide Y values range and adjust to bucket size
wideYAxisRange(min, max, tickInterval) { wideYAxisRange(min: number, max: number, tickInterval: number) {
let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2; let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2;
let y_min, y_max; let y_min, y_max;
@@ -284,7 +308,7 @@ export class StatusmapRenderer {
// Create svg element, add axes and // Create svg element, add axes and
// calculate sizes for cards drawing // calculate sizes for cards drawing
addHeatmapCanvas() { addStatusmapCanvas() {
let heatmap_elem = this.$heatmap[0]; let heatmap_elem = this.$heatmap[0];
this.width = Math.floor(this.$heatmap.width()) - this.padding.right; this.width = Math.floor(this.$heatmap.width()) - this.padding.right;
@@ -294,8 +318,10 @@ export class StatusmapRenderer {
this.heatmap.remove(); this.heatmap.remove();
} }
// Insert svg as a first child into heatmap_elem
// as the frozen tooltip moving logiс assumes that tooltip is the last child.
this.heatmap = d3.select(heatmap_elem) this.heatmap = d3.select(heatmap_elem)
.append("svg") .insert("svg",":first-child")
.attr("width", this.width) .attr("width", this.width)
.attr("height", this.height); .attr("height", this.height);
@@ -308,7 +334,10 @@ export class StatusmapRenderer {
this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND; this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND;
// calculate yOffset for YAxis // calculate yOffset for YAxis
this.yGridSize = Math.floor(this.chartHeight / this.cardsData.yBucketSize); this.yGridSize = this.chartHeight;
if (this.bucketMatrixPager.targets().length > 0) {
this.yGridSize = Math.floor(this.chartHeight / this.bucketMatrixPager.targets().length);
}
this.cardHeight = this.yGridSize ? this.yGridSize - this.cardVSpacing : 0; this.cardHeight = this.yGridSize ? this.yGridSize - this.cardVSpacing : 0;
this.yOffset = this.cardHeight / 2; this.yOffset = this.cardHeight / 2;
@@ -319,7 +348,7 @@ export class StatusmapRenderer {
// TODO allow per-y cardWidth! // TODO allow per-y cardWidth!
// we need to fill chartWidth with xBucketSize cards. // we need to fill chartWidth with xBucketSize cards.
this.xGridSize = this.chartWidth / (this.cardsData.xBucketSize+1); this.xGridSize = this.chartWidth / (this.bucketMatrix.xBucketSize+1);
this.cardWidth = this.xGridSize - this.cardHSpacing; this.cardWidth = this.xGridSize - this.cardHSpacing;
this.addXAxis(); this.addXAxis();
@@ -334,36 +363,41 @@ export class StatusmapRenderer {
} }
} }
addHeatmap() { addStatusmap():void {
this.addHeatmapCanvas(); let maxValue = this.panel.color.max || this.bucketMatrix.maxValue;
let minValue = this.panel.color.min || this.bucketMatrix.minValue;
let maxValue = this.panel.color.max || this.cardsData.maxValue;
let minValue = this.panel.color.min || this.cardsData.minValue;
if (this.panel.color.mode !== 'discrete') { if (this.panel.color.mode !== 'discrete') {
this.colorScale = this.getColorScale(maxValue, minValue); this.colorScale = this.getColorScale(maxValue, minValue);
} }
this.setOpacityScale(maxValue); this.setOpacityScale(maxValue);
let cards = this.heatmap.selectAll(".status-heatmap-card").data(this.cardsData.cards); // Draw cards from buckets.
cards.append("title"); this.heatmap.selectAll(".statusmap-cards-row").data(this.bucketMatrixPager.targets())
cards = cards.enter().append("rect") .enter()
.attr("cardId", c => c.id) .selectAll(".statustmap-card")
.attr("x", this.getCardX.bind(this)) .data((target:string) => this.bucketMatrix.buckets[target])
.attr("width", this.getCardWidth.bind(this)) .enter()
.attr("y", this.getCardY.bind(this)) .append("rect")
.attr("height", this.getCardHeight.bind(this)) .attr("cardId", (b:Bucket) => b.id)
.attr("rx", this.cardRound) .attr("xid", (b:Bucket) => b.xid)
.attr("ry", this.cardRound) .attr("yid", (b:Bucket) => b.yLabel)
.attr("class", "bordered status-heatmap-card") .attr("x", this.getCardX.bind(this))
.style("fill", this.getCardColor.bind(this)) .attr("width", this.getCardWidth.bind(this))
.style("stroke", this.getCardColor.bind(this)) .attr("y", this.getCardY.bind(this))
.style("stroke-width", 0) .attr("height", this.getCardHeight.bind(this))
//.style("stroke-width", getCardStrokeWidth) .attr("rx", this.cardRound)
//.style("stroke-dasharray", "3,3") .attr("ry", this.cardRound)
.style("opacity", this.getCardOpacity.bind(this)); .attr("class", (b:Bucket) => b.isEmpty() ? "empty-card" : "bordered statusmap-card")
.style("fill", this.getCardColor.bind(this))
.style("stroke", this.getCardColor.bind(this))
.style("stroke-width", 0)
//.style("stroke-width", getCardStrokeWidth)
//.style("stroke-dasharray", "3,3")
.style("opacity", this.getCardOpacity.bind(this));
let $cards = this.$heatmap.find(".status-heatmap-card"); // Set mouse events on cards.
let $cards = this.$heatmap.find(".statusmap-card + .bordered");
$cards $cards
.on("mouseenter", (event) => { .on("mouseenter", (event) => {
this.tooltip.mouseOverBucket = true; this.tooltip.mouseOverBucket = true;
@@ -376,7 +410,7 @@ export class StatusmapRenderer {
this._renderAnnotations(); this._renderAnnotations();
this.ctrl.events.emit('render-complete', { this.ctrl.events.emit(renderComplete, {
"chartWidth": this.chartWidth "chartWidth": this.chartWidth
}); });
} }
@@ -402,9 +436,6 @@ export class StatusmapRenderer {
getColorScale(maxValue, minValue = 0) { getColorScale(maxValue, minValue = 0) {
let colorScheme = _.find(this.ctrl.colorSchemes, {value: this.panel.color.colorScheme}); let colorScheme = _.find(this.ctrl.colorSchemes, {value: this.panel.color.colorScheme});
// if (!colorScheme) {
//
// }
let colorInterpolator = d3ScaleChromatic[colorScheme.value]; let colorInterpolator = d3ScaleChromatic[colorScheme.value];
let colorScaleInverted = colorScheme.invert === 'always' || let colorScaleInverted = colorScheme.invert === 'always' ||
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme); (colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
@@ -430,10 +461,12 @@ export class StatusmapRenderer {
} }
} }
getCardX(d) { getCardX(b: Bucket) {
let x; let x;
// cx is the center of the card. Card should be placed to the left. // cx is the center of the card. Card should be placed to the left.
let cx = this.xScale(d.x); //let cx = this.xScale(d.x);
let rightX = (b.relTo / this.bucketMatrix.rangeMs) * this.chartWidth;
let cx = rightX - this.cardWidth/2;
if (cx - this.cardWidth/2 < 0) { if (cx - this.cardWidth/2 < 0) {
x = this.yAxisWidth + this.cardHSpacing/2; x = this.yAxisWidth + this.cardHSpacing/2;
@@ -445,9 +478,13 @@ export class StatusmapRenderer {
} }
// xScale returns card center. Adjust cardWidth in case of overlaping. // xScale returns card center. Adjust cardWidth in case of overlaping.
getCardWidth(d) { getCardWidth(b: Bucket) {
//return 20;
let w; let w;
let cx = this.xScale(d.x);
let rightX = (b.relTo / this.bucketMatrix.rangeMs) * this.chartWidth;
let cx = rightX - this.cardWidth/2;
//let cx = this.xScale(d.x);
if (cx < this.cardWidth/2) { if (cx < this.cardWidth/2) {
// Center should not exceed half of card. // Center should not exceed half of card.
@@ -471,12 +508,15 @@ export class StatusmapRenderer {
return w; return w;
} }
getCardY(d) { // Top y for card.
return this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardVSpacing/2; // yScale gives ???
getCardY(b: Bucket) {
return this.yScale(b.yLabel) + this.chartTop - this.cardHeight - this.cardVSpacing/2;
} }
getCardHeight(d) { getCardHeight(b: Bucket) {
let ys = this.yScale(d.y); //return 20;
let ys = this.yScale(b.yLabel);
let y = ys + this.chartTop - this.cardHeight - this.cardVSpacing/2; let y = ys + this.chartTop - this.cardHeight - this.cardVSpacing/2;
let h = this.cardHeight; let h = this.cardHeight;
@@ -501,28 +541,32 @@ export class StatusmapRenderer {
return h; return h;
} }
getCardColor(d) { getCardColor(bucket: Bucket) {
if (this.panel.color.mode === 'opacity') { if (this.panel.color.mode === 'opacity') {
return this.panel.color.cardColor; return this.panel.color.cardColor;
} else if (this.panel.color.mode === 'spectrum') { } else if (this.panel.color.mode === 'spectrum') {
return this.colorScale(d.value); return this.colorScale(bucket.value);
} else if (this.panel.color.mode === 'discrete') { } else if (this.panel.color.mode === 'discrete') {
return this.ctrl.discreteHelper.getBucketColor(d.values); if (this.panel.seriesFilterIndex != null && this.panel.seriesFilterIndex != -1) {
return this.ctrl.discreteExtraSeries.getBucketColorSingle(bucket.values[this.panel.seriesFilterIndex]);
} else {
return this.ctrl.discreteExtraSeries.getBucketColor(bucket.values);
}
} }
} }
getCardOpacity(d) { getCardOpacity(bucket: Bucket) {
if (this.panel.nullPointMode === 'as empty' && d.value == null ) { if (this.panel.nullPointMode === 'as empty' && bucket.value == null ) {
return 0; return 0;
} }
if (this.panel.color.mode === 'opacity') { if (this.panel.color.mode === 'opacity') {
return this.opacityScale(d.value); return this.opacityScale(bucket.value);
} else { } else {
return 1; return 1;
} }
} }
getCardStrokeWidth(d) { getCardStrokeWidth(b: Bucket) {
if (this.panel.color.mode === 'discrete') { if (this.panel.color.mode === 'discrete') {
return '1'; return '1';
} }
@@ -572,13 +616,13 @@ export class StatusmapRenderer {
this.clearSelection(); this.clearSelection();
} }
onMouseLeave() { onMouseLeave(e) {
appEvents.emit('graph-hover-clear'); appEvents.emit(CoreEvents.graphHoverClear);
this.clearCrosshair(); this.clearCrosshair();
//annotationTooltip.destroy(); this.annotationTooltip.destroy();
} }
onMouseMove(event) { onMouseMove(event: MouseEvent) {
if (!this.heatmap) { return; } if (!this.heatmap) { return; }
const offset = this.getEventOffset(event); const offset = this.getEventOffset(event);
@@ -594,11 +638,19 @@ export class StatusmapRenderer {
//const pos = this.getEventPos(event, offset); //const pos = this.getEventPos(event, offset);
this.emitGraphHoverEvent(event); this.emitGraphHoverEvent(event);
this.drawCrosshair(offset.x); this.drawCrosshair(offset.x);
this.tooltip.show(event); //, data); // pos, this.data this.tooltip.show(event);
this.annotationTooltip.show(event); this.annotationTooltip.show(event);
} }
} }
// TODO emit an event and move logic to panelCtrl
public onMouseClick(e: MouseEvent) {
if (this.ctrl.panel.tooltip.freezeOnClick) {
this.tooltip.showFrozen(e);
this.tooltip.destroy();
}
}
getEventPos(event, offset) { getEventPos(event, offset) {
const x = this.xScale.invert(offset.x - this.yAxisWidth).valueOf(); const x = this.xScale.invert(offset.x - this.yAxisWidth).valueOf();
const y = this.yScale.invert(offset.y - this.chartTop); const y = this.yScale.invert(offset.y - this.chartTop);
@@ -631,7 +683,7 @@ export class StatusmapRenderer {
pos.panelRelY = Math.max(event.offsetY / this.height, 0.001); pos.panelRelY = Math.max(event.offsetY / this.height, 0.001);
// broadcast to other graph panels that we are hovering // broadcast to other graph panels that we are hovering
appEvents.emit('graph-hover', {pos: pos, panel: this.panel}); appEvents.emit(CoreEvents.graphHover, {pos: pos, panel: this.panel});
} }
limitSelection(x2) { limitSelection(x2) {
@@ -701,24 +753,23 @@ export class StatusmapRenderer {
} }
} }
render() { render() {
this.data = this.ctrl.data;
this.panel = this.ctrl.panel; this.panel = this.ctrl.panel;
this.timeRange = this.ctrl.range; this.timeRange = this.ctrl.range;
this.cardsData = this.ctrl.cardsData; this.bucketMatrix = this.ctrl.bucketMatrix;
this.bucketMatrixPager = this.ctrl.bucketMatrixPager;
if (!this.data || !this.cardsData || !this.setElementHeight()) { if (!this.bucketMatrix || !this.setElementHeight()) {
return; return;
} }
// Draw default axes and return if no data // Draw default axes and return if no data
if (_.isEmpty(this.cardsData.cards)) { this.addStatusmapCanvas();
this.addHeatmapCanvas(); if (this.bucketMatrix.noDatapoints) {
return; return;
} }
this.addHeatmap(); this.addStatusmap();
this.scope.yAxisWidth = this.yAxisWidth; this.scope.yAxisWidth = this.yAxisWidth;
this.scope.xAxisHeight = this.xAxisHeight; this.scope.xAxisHeight = this.xAxisHeight;
this.scope.chartHeight = this.chartHeight; this.scope.chartHeight = this.chartHeight;
@@ -739,7 +790,7 @@ export class StatusmapRenderer {
let annoData = _.map(this.ctrl.annotations, (d,i) => ({"x": Math.floor(this.yAxisWidth + this.xScale(d.time)), "id":i, "anno": d.source})); let annoData = _.map(this.ctrl.annotations, (d,i) => ({"x": Math.floor(this.yAxisWidth + this.xScale(d.time)), "id":i, "anno": d.source}));
//console.log({"ctrl_annotations": this.ctrl.annotations, "annoData": annoData}); //({"ctrl_annotations": this.ctrl.annotations, "annoData": annoData});
let anno = this.heatmap let anno = this.heatmap
.append("g") .append("g")
+168
View File
@@ -0,0 +1,168 @@
import { migratePanelConfig } from '../panel_config_migration';
describe('when migrate from config with urls', () => {
let panel: any;
describe('given full panel configuration', () => {
const panelConfig = {
"cards": {
"cardHSpacing": 2,
"cardMinWidth": 5,
"cardRound": null,
"cardVSpacing": 2
},
"color": {
"cardColor": "#b4ff00",
"colorScale": "sqrt",
"colorScheme": "interpolateGnYlRd",
"defaultColor": "#757575",
"exponent": 0.5,
"mode": "opacity",
"thresholds": []
},
"data": {
"decimals": null,
"unitFormat": "short"
},
"datasource": "TestData DB",
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"highlightCards": true,
"id": 4,
"legend": {
"show": false
},
"nullPointMode": "as empty",
"seriesFilterIndex": -1,
"targets": [
{
"aggregation": "Last",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1,3,3"
},
"decimals": 2,
"displayAliasType": "Warning / Critical",
"displayType": "Regular",
"displayValueWithAlias": "Never",
"refId": "A",
"scenarioId": "predictable_csv_wave",
"stringInput": "",
"units": "none",
"valueHandler": "Number Threshold"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"show": true
},
"type": "flant-statusmap-panel",
"urls": [
{
"base_url": "",
"extraSeries": {
"index": -1
},
"forcelowercase": true,
"icon_fa": "external-link",
"label": "",
"tooltip": "",
"useExtraSeries": false,
"useseriesname": true
}
],
"useMax": true,
"usingUrl": true,
"xAxis": {
"labelFormat": "%a %m/%d",
"show": true
},
"yAxis": {
"maxWidth": -1,
"minWidth": -1,
"show": true
},
"yAxisSort": "metrics"
}
beforeEach(() => {
panel = Object.assign({}, panelConfig);
migratePanelConfig(panel);
});
it('should migrate usingUrls to tooltip.freezeOnClick', () => {
expect(panel.usingUrl).toBeUndefined();
expect(panel.tooltip).toHaveProperty("freezeOnClick", true);
expect(panel.tooltip.freezeOnClick).toBeTruthy();
});
it('should have no urls', () => {
expect(panel.urls).toBeUndefined();
});
it('should have empty items in tooltip', () => {
expect(panel.tooltip).toHaveProperty("items", []);
expect(panel.tooltip.items).toHaveLength(0);
});
});
describe("given configuration with non-empty base_url", () => {
describe("with variables", () => {
const panelConfig = {
"tooltip": {
"show": true
},
"urls": [
{
"base_url": "https://google.com/$series_label$time",
"extraSeries": {
"index": -1
},
"forcelowercase": true,
"icon_fa": "external-link",
"label": "google",
"tooltip": "",
"useExtraSeries": false,
"useseriesname": true,
},
{
"base_url": "example.com/$series_extra",
"extraSeries": {
"format": "YYYY/MM/DD/HH_mm_ss",
"index": -1
},
"forcelowercase": true,
"icon_fa": "external-link",
"label": "DateLink",
"useExtraSeries": true,
}
]
};
beforeEach(() => {
panel = Object.assign({}, panelConfig);
migratePanelConfig(panel);
});
it("should have equal size of tooltip.items", () => {
expect(panel.tooltip).toHaveProperty("items");
expect(panel.tooltip.items).toHaveLength(panelConfig.urls.length);
});
it("should replace time variable with __url_time_range", () => {
expect(panel.tooltip.items[0].urlTemplate).toContain("${__url_time_range}");
});
it("should replace series_label variable with __y_label", () => {
expect(panel.tooltip.items[0].urlTemplate).toContain("${__y_label}");
});
it("should replace series_extra variable with __value_date", () => {
expect(panel.tooltip.items[1].urlTemplate).toContain("${__value_date}");
});
});
});
});
+206 -36
View File
@@ -1,45 +1,215 @@
// A holder of a group of values
class Bucket {
// uniq id
id: number = 0;
// Array of values in this bucket
values: any[] = [];
columns: any[] = []; // From pr/86
// a bucket has multiple values
multipleValues: boolean = false;
// a bucket has values that has no mapped color
noColorDefined: boolean = false;
// y label
y: string = "";
yLabel: string = "";
// This value can be used to calculate a x coordinate on a graph
x: number = 0;
xid: number = 0;
// a time range of this bucket
from: number = 0;
to: number = 0;
// to and from relative to real "from"
relFrom: number = 0;
relTo: number = 0;
mostRecent: boolean = false;
// A holder of values // Saved minimum and maximum of values in this bucket
class Card { minValue: number = 0;
// uniq maxValue: number = 0;
id: number = 0; // A value if multiple values is not allowed
// Array of values in this bucket value: number = 0;
values: any[] = [];
// card has multiple values
multipleValues: boolean = false;
// card has values that has no color
noColorDefined: boolean = false;
//
y: string = "";
//
x: number = 0;
//
minValue: number = 0;
maxValue: number = 0;
value: number = 0;
constructor() {
} constructor() {
} }
class CardsStorage {
cards: Card[] = [];
xBucketSize: number = 0;
yBucketSize: number = 0;
maxValue: number;
minValue: number;
multipleValues: boolean = false;
noColorDefined: boolean = false;
targets: string[] = [];
targetIndex: any;
constructor() { belong(ts: number): boolean {
this.maxValue = 0; return ts >= this.from && ts <= this.to;
this.minValue = 0; }
put(value: any) {
this.values.push(value);
}
done() {
// calculate min, max, value
}
isEmpty(): boolean {
return this.values.length == 0;
}
}
class BucketMatrix {
// buckets for each y label
buckets: {[yLabel: string]: Bucket[]} = {};
maxValue: number = 0;
minValue: number = 0;
multipleValues: boolean = false;
noColorDefined: boolean = false;
// a flag that indicate that buckets has stub values
noDatapoints: boolean = false;
// An array of row labels
targets: string[] = [];
rangeMs: number = 0;
intervalMs: number = 0;
xBucketSize: number = 0; // TODO remove: a transition from CardsData
constructor() {
}
get(yid: string, xid: number): Bucket {
if (yid in this.buckets) {
if (xid in this.buckets[yid]) {
return this.buckets[yid][xid];
}
}
return new Bucket();
}
hasData(): boolean {
let hasData = false;
if (this.targets.length > 0) {
this.targets.map((target:string) => {
if (this.buckets[target].length > 0) {
hasData = true;
}
});
}
return hasData;
}
}
export var pagerChanged:any = {name:'statusmap-pager-changed'};
class BucketMatrixPager {
bucketMatrix: BucketMatrix;
enable: boolean;
defaultPageSize: number = -1;
pageSize: number = -1;
currentPage: number = 0;
constructor() {
let m = new BucketMatrix();
this.bucketMatrix = m;
}
// An array of row labels for current page.
targets(): string[] {
if (!this.enable) {
return this.bucketMatrix.targets;
}
return this.bucketMatrix.targets.slice(this.pageSize * this.currentPage, this.pageSize * (this.currentPage+1) );
}
buckets(): {[yLabel: string]: Bucket[]} {
if (!this.enable) {
return this.bucketMatrix.buckets;
}
let buckets: {[yLabel: string]: Bucket[]} = {}
let me = this;
this.targets().map(function (rowLabel) {
buckets[rowLabel] = me.bucketMatrix.buckets[rowLabel];
})
return buckets;
}
setEnable(enable: boolean) {
this.enable = enable;
}
setCurrent(num: number): void {
this.currentPage = num;
}
setDefaultPageSize(num: number): void {
this.defaultPageSize = num;
}
setPageSize(num: number): void {
this.pageSize = num;
}
pages(): number {
return Math.ceil(this.totalRows() / this.pageSize);
}
totalRows(): number {
return this.bucketMatrix.targets.length;
}
pageStartRow(): number {
if (!this.enable) {
return 1;
}
return (this.pageSize * this.currentPage) + 1;
}
pageEndRow(): number {
if (!this.enable) {
return this.totalRows();
}
let last = this.pageSize * (this.currentPage+1);
if (last > this.totalRows()) {
return this.totalRows();
}
return last;
}
hasNext(): boolean {
if (!this.enable) {
return false;
}
if ((this.currentPage+1)*this.pageSize >= this.totalRows()) {
return false;
}
return true;
// currentPage >= ctrl.bucketMatrix.targets.length/ctrl.pageSizeViewer - 1 || ctrl.numberOfPages == 0
}
hasPrev(): boolean {
if (!this.enable) {
return false;
}
return this.currentPage > 0;
}
switchToNext() {
if (this.hasNext()) {
this.currentPage = this.currentPage + 1;
} }
} }
export {CardsStorage, Card}; switchToPrev() {
if (this.hasPrev()) {
this.currentPage = this.currentPage - 1;
}
}
}
export {Bucket, BucketMatrix, BucketMatrixPager };
+198 -45
View File
@@ -2,25 +2,34 @@ import d3 from 'd3';
import $ from 'jquery'; import $ from 'jquery';
import _ from 'lodash'; import _ from 'lodash';
import { StatusHeatmapCtrl } from './module';
let TOOLTIP_PADDING_X = 30; let TOOLTIP_PADDING_X = 30;
let TOOLTIP_PADDING_Y = 5; let TOOLTIP_PADDING_Y = 5;
// TODO rename file to tooltip_ctrl.ts
// TODO move DefaultValueDateFormat into tooltip.ts, as it used in several places (migration, partial and tooltip render)
let DefaultValueDateFormat = 'YYYY/MM/DD/HH_mm_ss';
export class StatusmapTooltip { export class StatusmapTooltip {
tooltip: any; tooltip: any;
scope: any; scope: any;
dashboard: any; dashboard: any;
panelCtrl: any; panelCtrl: StatusHeatmapCtrl;
panel: any; panel: any;
heatmapPanel: any; panelElem: any;
mouseOverBucket: any; mouseOverBucket: any;
originalFillColor: any; originalFillColor: any;
tooltipWidth: number;
tooltipFrozen: any;
constructor(elem: any, scope: any) { constructor(elem: any, scope: any) {
this.scope = scope; this.scope = scope;
this.dashboard = scope.ctrl.dashboard; this.dashboard = scope.ctrl.dashboard;
this.panelCtrl = scope.ctrl; this.panelCtrl = scope.ctrl;
this.panel = scope.ctrl.panel; this.panel = scope.ctrl.panel;
this.heatmapPanel = elem; this.panelElem = elem;
this.mouseOverBucket = false; this.mouseOverBucket = false;
this.originalFillColor = null; this.originalFillColor = null;
@@ -33,7 +42,7 @@ export class StatusmapTooltip {
if (!this.tooltip) { if (!this.tooltip) {
this.add(); this.add();
this.move(e); this.move(e, this.tooltip);
} }
} }
@@ -44,13 +53,13 @@ export class StatusmapTooltip {
onMouseMove(e) { onMouseMove(e) {
if (!this.panel.tooltip.show) { return; } if (!this.panel.tooltip.show) { return; }
this.move(e); this.move(e, this.tooltip);
} }
add() { add() {
this.tooltip = d3.select("body") this.tooltip = d3.select("body")
.append("div") .append("div")
.attr("class", "statusmap-tooltip graph-tooltip grafana-tooltip"); .attr("class", "graph-tooltip statusmap-tooltip");
} }
destroy() { destroy() {
@@ -61,59 +70,96 @@ export class StatusmapTooltip {
this.tooltip = null; this.tooltip = null;
} }
show(pos) { removeFrozen() {
if (this.tooltipFrozen) {
this.tooltipFrozen.remove();
this.tooltipFrozen = null
}
}
showFrozen(pos: any) {
this.removeFrozen();
this.tooltipFrozen = d3.select(this.panelElem[0])
.append("div")
.attr("class", "graph-tooltip statusmap-tooltip statusmap-tooltip-frozen");
this.displayTooltip(pos, this.tooltipFrozen, true);
this.moveRelative(pos, this.tooltipFrozen);
}
show(pos: any) {
if (!this.panel.tooltip.show || !this.tooltip) { return; } if (!this.panel.tooltip.show || !this.tooltip) { return; }
// shared tooltip mode
// TODO support for shared tooltip mode
if (pos.panelRelY) { if (pos.panelRelY) {
return; return;
} }
let cardId = d3.select(pos.target).attr('cardId');
if (!cardId) { this.displayTooltip(pos, this.tooltip, false);
this.move(pos, this.tooltip);
}
// Retrieve bucket and create html content inside tooltips div element.
displayTooltip(pos: any, tooltip: any, frozen: boolean) {
let cardEl = d3.select(pos.target);
let yid = cardEl.attr('yid');
let xid = cardEl.attr('xid');
let bucket = this.panelCtrl.bucketMatrix.get(yid, xid); // TODO string-to-number conversion for xid
if (!bucket || bucket.isEmpty()) {
this.destroy(); this.destroy();
return; return;
} }
let card = this.panelCtrl.cardsData.cards[cardId]; let timestamp = bucket.to;
if (!card) { let yLabel = bucket.yLabel;
this.destroy(); let value = bucket.value;
return; let values = bucket.values;
// TODO create option for this formatting.
let tooltipTimeFormat:string = 'YYYY-MM-DD HH:mm:ss';
let time: Date = this.dashboard.formatDate(+timestamp, tooltipTimeFormat);
// Close button for the frozen tooltip.
let tooltipClose:string = ``;
if (frozen) {
tooltipClose = `
<a class="pointer pull-right small tooltip-close">
<i class="fa fa-remove"></i>
</a>
`;
} }
let x = card.x; let tooltipHtml:string = `<div class="graph-tooltip-time">${time}${tooltipClose}</div>`;
let y = card.y;
let value = card.value;
let values = card.values;
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
let time = this.dashboard.formatDate(+x, tooltipTimeFormat);
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
<div class="statusmap-histogram"></div>`;
if (this.panel.color.mode === 'discrete') { if (this.panel.color.mode === 'discrete') {
let statuses = this.panelCtrl.discreteHelper.convertValuesToTooltips(values); let statuses;
let statusesHtml = ''; if (this.panel.seriesFilterIndex >= 0) {
if (statuses.length === 1) { statuses = this.panelCtrl.discreteExtraSeries.convertValueToTooltips(value);
statusesHtml = "status:"; } else {
} else if (statuses.length > 1) { statuses = this.panelCtrl.discreteExtraSeries.convertValuesToTooltips(values);
statusesHtml = "statuses:"; }
let statusTitle:string = "status:";
if (statuses.length > 1) {
statusTitle = "statuses:";
} }
tooltipHtml += ` tooltipHtml += `
<div> <div>
name: <b>${y}</b> <br> name: <b>${yLabel}</b>
${statusesHtml} <br>
<span>${statusTitle}</span>
<ul> <ul>
${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}" class="discrete-item">${v.tooltip}</li>`), "")} ${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}; text-align:center" class="discrete-item">${v.tooltip}</li>`), "")}
</ul> </ul>
</div>`; </div>`;
} else { } else {
if (values.length === 1) { if (values.length === 1) {
tooltipHtml += `<div> tooltipHtml += `<div>
name: <b>${y}</b> <br> name: <b>${name}</b> <br>
value: <b>${value}</b> <br> value: <b>${value}</b> <br>
</div>`; </div>`;
} else { } else {
tooltipHtml += `<div> tooltipHtml += `<div>
name: <b>${y}</b> <br> name: <b>${name}</b> <br>
values: values:
<ul> <ul>
${_.join(_.map(values, v => `<li>${v}</li>`), "")} ${_.join(_.map(values, v => `<li>${v}</li>`), "")}
@@ -122,35 +168,107 @@ export class StatusmapTooltip {
} }
} }
// "Ambiguous bucket state: Multiple values!"; tooltipHtml += `<div class="statusmap-histogram"></div>`;
if (!this.panel.useMax && card.multipleValues) {
if (this.panel.tooltip.showItems) {
// Additional information: urls, etc.
// Clone additional items
let items: any = JSON.parse(JSON.stringify(this.panel.tooltip.items));
let scopedVars = {};
let valueVar;
for (let i = 0; i < bucket.values.length; i++) {
valueVar = `__value_${i}`;
scopedVars[valueVar] = {value: bucket.values[i] };
}
scopedVars[`__value`] = {value: bucket.value};
scopedVars[`__y_label`] = {value: yLabel};
// Grafana 7.0 compatible
scopedVars[`__url_time_range`] = {value: this.panelCtrl.retrieveTimeVar()};
for (let item of items) {
if (_.isEmpty(item.urlTemplate)) {
item.link = "#";
} else {
let dateFormat = item.valueDateFormat;
if (dateFormat == '') {
dateFormat = DefaultValueDateFormat;
}
let valueDateVar;
for (let i = 0; i < bucket.values.length; i++) {
valueDateVar = `__value_${i}_date`;
scopedVars[valueDateVar] = {value: this.dashboard.formatDate(+bucket.values[i], dateFormat)};
}
scopedVars[`__value_date`] = {value: this.dashboard.formatDate(+bucket.value, dateFormat)};
item.link = this.panelCtrl.templateSrv.replace(item.urlTemplate, scopedVars)
// Force lowercase for link
if (item.urlToLowerCase) {
item.link = item.link.toLowerCase();
}
}
item.label = item.urlText;
if (_.isEmpty(item.label)) {
item.label = _.isEmpty(item.urlTemplate) ? "Empty URL" : _.truncate(item.link);
}
}
tooltipHtml += _.join(_.map(items, v => `
<div>
<a href="${v.link}" target="_blank">
<div class="dashlist-item">
<p class="dashlist-link dashlist-link-dash-db">
<span style="word-wrap: break-word;" class="dash-title">${v.label}</span><span class="dashlist-star">
<i class="fa fa-${v.urlIcon}"></i>
</span></p> </div></a><div>`), "\n");
}
// Ambiguous state: there multiple values in bucket!
// TODO rename useMax to expectMultipleValues
if (!this.panel.useMax && bucket.multipleValues) {
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.multipleValues.title}</div>`; tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.multipleValues.title}</div>`;
} }
// Discrete mode errors // Discrete mode errors
if (this.panel.color.mode === 'discrete') { if (this.panel.color.mode === 'discrete') {
if (card.noColorDefined) { if (bucket.noColorDefined) {
let badValues = this.panelCtrl.discreteHelper.getNotColoredValues(values); let badValues = this.panelCtrl.discreteExtraSeries.getNotColoredValues(values);
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.noColorDefined.title} tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.noColorDefined.title}
<br>not colored values: <br>not colored values:
<ul> <ul>
${_.join(_.map(badValues, v => `<li>${v}</li>`), "")} ${_.join(_.map(badValues, v => `<li>${v}</li>`), "")}
</ul> </ul>
</div>`; </div>`;
} }
} }
this.tooltip.html(tooltipHtml); tooltip.html(tooltipHtml);
this.move(pos); // Assign mouse event handlers for "frozen" tooltip.
if (frozen) {
// Stop propagation mouse events up to parents to allow interaction with frozen tooltips elements.
tooltip
.on("click", function(){d3.event.stopPropagation(); })
.on("mousedown", function(){d3.event.stopPropagation(); })
.on("mouseup", function(){d3.event.stopPropagation(); });
// Activate close button
tooltip.select("a.tooltip-close")
.on("click", this.removeFrozen.bind(this));
}
} }
move(pos) { // Move tooltip as absolute positioned element.
if (!this.tooltip) { return; } move(pos, tooltip) {
if (!tooltip) { return; }
let elem = $(this.tooltip.node())[0]; let elem = $(tooltip.node())[0];
let tooltipWidth = elem.clientWidth; let tooltipWidth = elem.clientWidth;
this.tooltipWidth = tooltipWidth;
let tooltipHeight = elem.clientHeight; let tooltipHeight = elem.clientHeight;
let left = pos.pageX + TOOLTIP_PADDING_X; let left = pos.pageX + TOOLTIP_PADDING_X;
@@ -164,8 +282,43 @@ export class StatusmapTooltip {
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y; top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
} }
return this.tooltip return tooltip
.style("left", left + "px") .style("left", left + "px")
.style("top", top + "px"); .style("top", top + "px");
} }
// Move tooltip relative to svg element of panel.
moveRelative(pos, tooltip) {
if (!tooltip) { return; }
let panelX = pos.pageX - this.panelElem.offset().left;
let panelY = pos.pageY - this.panelElem.offset().top;
let panelWidth = this.panelElem.width();
let panelHeight = this.panelElem.height();
// 'position: relative' sets tooltips width to 100% of panel element.
// Restore width from floating tooltip and add more space for 'Close' button.
let tooltipWidth = this.tooltipWidth + 25;
// Left property is clamped so tooltip stays inside panel bound box.
let tooltipLeft = panelX + TOOLTIP_PADDING_X;
if (tooltipLeft + tooltipWidth > panelWidth) {
tooltipLeft = panelWidth - tooltipWidth;
}
if (tooltipLeft < 0 ) {
tooltipLeft = 0;
}
// Frozen tooltips root element is appended next to panels svg element,
// so top property is adjusted to move tooltips root element
// up to the mouse pointer position.
let tooltipTop = -(panelHeight - panelY + TOOLTIP_PADDING_Y);
return tooltip
.style("left", tooltipLeft + "px")
.style("top", tooltipTop + "px")
.style("width", tooltipWidth + "px");
}
} }
+61
View File
@@ -0,0 +1,61 @@
import { StatusHeatmapCtrl } from './module';
let emptyTooltipItem = {
urlText: '',
urlTemplate: '',
urlIcon: 'external-link',
urlToLowerCase: true,
valueDateFormat: ''
};
export class TooltipEditorCtrl {
panel: any;
panelCtrl: StatusHeatmapCtrl;
constructor($scope: any) {
$scope.editor = this;
this.panelCtrl = $scope.ctrl as StatusHeatmapCtrl;
this.panel = this.panelCtrl.panel;
}
render() {
this.panelCtrl.render();
}
onAddUrl() {
this.panel.tooltip.items.push(Object.assign({}, emptyTooltipItem));
this.render();
}
onRemoveUrl(index: number) {
this.panel.tooltip.items.splice(index, 1);
this.render();
}
onRemoveUrls() {
this.panel.tooltip.items = [];
this.render();
}
getFAIconClasses() {
return ['external-link', 'plus', 'anchor', 'ban', 'globe', 'gear', 'cloud', 'download', 'cloud-download'];
}
getValueDateFormats() {
return [
'YYYY/MM/DD/HH_mm_ss',
'YYYYMMDDHHmmss',
'YYYY-MM-DD-HH-mm-ss'
];
}
}
export function tooltipEditorCtrl() {
'use strict';
return {
restrict: 'E',
scope: true,
templateUrl: 'public/plugins/flant-statusmap-panel/partials/tooltip_editor.html',
controller: TooltipEditorCtrl,
};
}
+2 -2
View File
@@ -30,12 +30,12 @@
"noImplicitReturns": true, "noImplicitReturns": true,
"noImplicitThis": true, "noImplicitThis": true,
"noImplicitUseStrict": false, "noImplicitUseStrict": false,
"noImplicitAny": true, "noImplicitAny": false,
"noEmitOnError": false, "noEmitOnError": false,
"downlevelIteration": true, "downlevelIteration": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"pretty": true, "pretty": true,
"typeRoots": ["./node_modules/@types"] //,/* ,"src/types"*/ "typeRoots": ["./node_modules/@types" ] // , "./src/types" ]
//"types": [ //"types": [
// "lodash" // "lodash"
//] //]