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
This commit is contained in:
Ivan Mikheykin
2020-07-09 13:16:09 +03:00
parent 4e281e53d5
commit 8c7279561d
44 changed files with 3967 additions and 1513 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ coreModule.directive('optionsColorLegend', function() {
return {
restrict: 'E',
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 panel = scope.ctrl.panel;
+16 -13
View File
@@ -41,6 +41,19 @@
}
.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;
font-size: $font-size-sm;
background-color: $graph-tooltip-bg;
@@ -54,19 +67,9 @@
}
}
.statusmap-tooltip-extraseries {
white-space: nowrap;
font-size: $font-size-sm;
background-color: $graph-tooltip-bg;
color: $text-color;
.discrete-item {
color: #ffffff;
padding: 1px;
font-weight: bold;
margin-bottom: 0;
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 {
-9
View File
@@ -1,9 +0,0 @@
export enum ExtraSeriesFormat {
Date = 'Date',
Raw = 'Raw',
}
export enum ExtraSeriesFormatValue {
Date = 'YYYY/MM/DD/HH_mm_ss',
Raw = '',
}
+21 -273
View File
@@ -4,6 +4,9 @@ import { auto } from 'angular';
// Components
import './color_legend';
import { optionsEditorCtrl } from './options_editor';
import { tooltipEditorCtrl } from './tooltip_editor';
import { migratePanelConfig } from './panel_config_migration';
// Utils
import kbn from 'app/core/utils/kbn';
@@ -14,12 +17,8 @@ import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { AnnotationsSrv } from 'app/features/annotations/annotations_srv';
import { Bucket, BucketMatrix } from './statusmap_data';
import rendering from './rendering';
// import aggregates, { aggregatesMap } from './aggregates';
// import fragments, { fragmentsMap } from './fragments';
// import { labelFormats } from './xAxisLabelFormats';
import {statusHeatmapOptionsEditor} from './options_editor';
import {ColorModeDiscrete} from "./color_mode_discrete";
import { ExtraSeriesFormat, ExtraSeriesFormatValue } from './extra_series_format';
const VALUE_INDEX = 0,
TIME_INDEX = 1;
@@ -56,7 +55,6 @@ const colorSchemes = [
let colorModes = ['opacity', 'spectrum', 'discrete'];
let opacityScales = ['linear', 'sqrt'];
loadPluginCss({
dark: 'plugins/flant-statusmap-panel/css/statusmap.dark.css',
light: 'plugins/flant-statusmap-panel/css/statusmap.light.css'
@@ -81,7 +79,6 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
noDatapoints: boolean;
discreteExtraSeries: ColorModeDiscrete;
extraSeriesFormats: any = [];
annotations: object[] = [];
annotationsPromise: any;
@@ -108,68 +105,41 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
cardRound: null
},
xAxis: {
show: true,
labelFormat: '%a %m/%d'
show: true
},
yAxis: {
show: true,
minWidth: -1,
maxWidth: -1,
maxWidth: -1
},
tooltip: {
show: true
show: true,
freezeOnClick: true,
showItems: false,
items: [] // see tooltip_editor.ts
},
legend: {
show: true
},
data: {
unitFormat: 'short',
decimals: null
},
// how null points should be handled
nullPointMode: 'as empty',
yAxisSort: 'metrics',
highlightCards: true,
useMax: true,
urls: [{
tooltip: '',
label: '',
base_url: '',
useExtraSeries: false,
useseriesname: true,
forcelowercase: true,
icon_fa: 'external-link',
extraSeries: {
index: -1
}
}],
seriesFilterIndex: -1,
usingUrl: false
seriesFilterIndex: -1
};
/** @ngInject */
constructor($scope: any, $injector: auto.IInjectorService, timeSrv, private annotationsSrv: AnnotationsSrv, $window, datasourceSrv, variableSrv, templateSrv) {
constructor($scope: any, $injector: auto.IInjectorService, timeSrv, private annotationsSrv: AnnotationsSrv, $window, datasourceSrv, public variableSrv: any, templateSrv) {
super($scope, $injector);
migratePanelConfig(this.panel);
_.defaultsDeep(this.panel, this.panelDefaults);
this.opacityScales = opacityScales;
this.colorModes = colorModes;
this.colorSchemes = colorSchemes;
this.variableSrv = variableSrv;
this.extraSeriesFormats = ExtraSeriesFormat;
this.renderLink = (link, scopedVars, format) => {
var scoped = {}
for (var key in scopedVars) {
scoped[key] = { value: scopedVars[key] }
}
if (format) {
return this.templateSrv.replace(link, scoped, format)
} else {
return this.templateSrv.replace(link, scoped)
}
}
// default graph width for discrete card width calculation
this.graph = {
@@ -209,38 +179,15 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.events.on('refresh', this.postRefresh.bind(this));
// custom event from rendering.js
this.events.on('render-complete', this.onRenderComplete.bind(this));
this.events.on('onChangeType', this.onChangeType.bind(this));
this.onCardColorChange = this.onCardColorChange.bind(this);
}
onRenderComplete(data: any):void {
// console.log({
// data: this.data,
// bucketMatrix: this.bucketMatrix,
// chartWidth: data.chartWidth,
// from: this.range.from.valueOf(),
// to: this.range.to.valueOf()
// })
this.graph.chartWidth = data.chartWidth;
this.renderingCompleted();
}
onChangeType(url): void {
switch (url.type) {
case ExtraSeriesFormat.Date:
url.extraSeries.format = ExtraSeriesFormatValue.Date;
break;
case ExtraSeriesFormat.Raw:
url.extraSeries.format = ExtraSeriesFormatValue.Raw;
break;
default:
url.extraSeries.format = ExtraSeriesFormatValue.Raw;
break;
}
}
// getChartWidth returns an approximation of chart canvas width or
// a saved value calculated during a render.
getChartWidth():number {
@@ -266,6 +213,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
// 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();
@@ -316,17 +264,8 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.interval = interval;
// Get final buckets count after interval is adjusted
// TODO is it needed?
//this.xBucketsCount = Math.floor(rangeMs / intervalMs);
// console.log("calculateInterval: ", {
// interval: this.interval,
// intervalMs: this.intervalMs,
// rangeMs: rangeMs,
// from: this.range.from.valueOf(),
// to: this.range.to.valueOf(),
// numIntervals: rangeMs/this.intervalMs,
// maxCardsCount: maxCardsCount,
// });
}
issueQueries(datasource: any) {
@@ -342,8 +281,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
* (but not wait for completion). This resolves
* 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)) {
return this.annotationsSrv.datasourcePromises.then(r => {
return this.issueQueriesWithInterval(datasource, this.interval);
@@ -364,7 +302,6 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
return res;
}
onDataReceived(dataList: any) {
this.data = dataList;
// Quick workaround for 7.0+. There is no call to
@@ -401,7 +338,8 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
}
onInitEditMode() {
this.addEditorTab('Options', statusHeatmapOptionsEditor, 2);
this.addEditorTab('Options', optionsEditorCtrl, 2);
this.addEditorTab('Tooltip', tooltipEditorCtrl, 3);
this.unitFormats = kbn.getUnitFormats();
}
@@ -436,8 +374,6 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
if (this.bucketMatrix) {
this.noDatapoints = this.bucketMatrix.noDatapoints;
}
//console.log(this);
}
onCardColorChange(newColor) {
@@ -455,75 +391,14 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.noColorDefined = false;
}
onEditorAddThreshold() {
this.panel.color.thresholds.push({ color: this.panel.defaultColor });
this.render();
}
onEditorAddUrl = () => {
this.panel.urls.push({
label: '',
base_url: '',
useExtraSeries: false,
useseriesname: true,
forcelowercase: true,
icon_fa: 'external-link',
extraSeries: {
index: -1
}
});
this.render();
}
onEditorRemoveUrl = (index) => {
this.panel.urls.splice(index, 1);
this.render();
}
onEditorRemoveThreshold(index:number) {
this.panel.color.thresholds.splice(index, 1);
this.render();
}
onEditorRemoveThresholds() {
this.panel.color.thresholds = [];
this.render();
}
onEditorRemoveUrls = () => {
this.panel.urls = [];
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) {
rendering(scope, elem, attrs, ctrl);
}
// Compatible with Grafana 7.0 overrides feature.
retrieveTimeVar() {
var time = this.timeSrv.timeRangeForUrl();
var var_time = '&from=' + time.from + '&to=' + time.to;
return var_time;
return 'from=' + time.from + '&to=' + time.to;
}
// convertToBuckets groups values in data into buckets by target and timestamp.
@@ -737,133 +612,6 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
bucketMatrix.targets = targetKeys;
return bucketMatrix;
this.bucketMatrix = 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);
// }
// }
// }
// let cardsData = <CardsStorage> {
// cards: [],
// 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;}
// // Collect uniq timestamps from data and spread over targets and timestamps
// // collect uniq targets and their indices
// _.map(data, (d, i) => {
// cardsData.targetIndex[d.target] = _.concat(_.toArray(cardsData.targetIndex[d.target]), i)
// });
// // TODO add some logic for targets heirarchy
// cardsData.targets = _.keys(cardsData.targetIndex);
// cardsData.yBucketSize = cardsData.targets.length;
// // Maximum number of buckets over x axis
// cardsData.xBucketSize = _.max(_.map(data, d => d.datapoints.length));
// // Collect all values for each bucket from datapoints with similar target.
// // TODO aggregate values into buckets over datapoint[TIME_INDEX] not over datapoint index (j).
// for(let i = 0; i < cardsData.targets.length; i++) {
// let target = cardsData.targets[i];
// 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;
}
}
+48 -6
View File
@@ -1,26 +1,68 @@
import kbn from 'app/core/utils/kbn';
import { StatusHeatmapCtrl } from './module';
export class StatusHeatmapOptionsEditorCtrl {
panel: any;
panelCtrl: any;
panelCtrl: StatusHeatmapCtrl;
unitFormats: any;
constructor($scope) {
/** @ngInject */
constructor($scope: any) {
$scope.editor = this;
this.panelCtrl = $scope.ctrl;
this.panelCtrl = $scope.ctrl as StatusHeatmapCtrl;
this.panel = this.panelCtrl.panel;
this.unitFormats = kbn.getUnitFormats();
this.panelCtrl.render();
this.unitFormats = kbn.getUnitFormats();
}
setUnitFormat(subItem) {
this.panel.data.unitFormat = subItem.value;
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';
return {
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;
}
+20 -117
View File
@@ -81,7 +81,7 @@
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveThreshold($index)">
<a class="pointer" tabindex="1" ng-click="editor.onRemoveThreshold($index)">
<i class="fa fa-trash"/>
</a>
</label>
@@ -91,12 +91,12 @@
<div class="gf-form-inline">
<div class="gf-form"></div>
<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
</button>
</div>
<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
</button>
</div>
@@ -108,12 +108,12 @@
<label class="gf-form-label">Presets</label>
</div>
<div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddThreeLights()">
<button class="btn" ng-click="editor.onAddThreeLights()">
Red-Yellow-Green
</button>
</div>
<div class="gf-form">
<button class="btn" ng-click="ctrl.onEditorAddSolarized()">
<button class="btn" ng-click="editor.onAddSolarized()">
Solarized
</button>
</div>
@@ -144,10 +144,6 @@
label="Show legend"
checked="ctrl.panel.legend.show" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-8"
label="Show tooltip"
checked="ctrl.panel.tooltip.show" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-8"
label="Show X axis"
checked="ctrl.panel.xAxis.show" on-change="ctrl.render()">
@@ -159,17 +155,12 @@
<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.xAxis.minWidth" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.render()" ng-model-onblur>
<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.xAxis.maxWidth" class="gf-form-input width-5" placeholder="auto" data-placement="right" bs-tooltip="''" ng-change="ctrl.render()" ng-model-onblur>
<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>
<div class="section gf-form-group">
<h5 class="section-heading">Buckets</h5>
<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">
@@ -180,6 +171,11 @@
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Buckets</h5>
<gf-form-switch class="gf-form" label-class="width-8"
label="Multiple values"
checked="ctrl.panel.useMax" on-change="ctrl.render()">
@@ -187,6 +183,13 @@
<div class="gf-form" ng-show="ctrl.panel.useMax">
Color for bucket with multiple values is determined by color mode<br/>
</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">
<label class="gf-form-label width-8">Display nulls</label>
@@ -194,6 +197,7 @@
<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 class="gf-form">
<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>
@@ -212,105 +216,4 @@
</div>
</div>
<div class="section gf-form-group width-30">
<h5 class="section-heading">Filtering</h5>
<div class="gf-form">
<label class="gf-form-label width-9">Show only serie with Index</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right"
bs-tooltip="'Filter with the serie index to show the value on graph. Use -1 to get all values'" ng-model="ctrl.panel.seriesFilterIndex" ng-change="ctrl.refresh()"
ng-model-onblur>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">URL</h5>
<div class="gf-form">
Insert the url to navigate<br />
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label="Show extraSeries Tooltip when clicking elements" label-class="width-12"
checked="ctrl.panel.usingUrl" on-change="ctrl.render()"></gf-form-switch>
</div>
<div class="section gf-form-group" ng-if="ctrl.panel.usingUrl">
<br>
<div class="gf-form-inline">
<div class="gf-form"></div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorAddUrl()">
<i class="fa fa-plus"></i> Add new URL
</button>
</div>
<div class="gf-form">
<button class="btn btn-inverse" ng-click="ctrl.onEditorRemoveUrls()">
<i class="fa fa-minus"></i> Remove all URLs
</button>
</div>
</div>
<div class="gf-form-block" ng-repeat="url in ctrl.panel.urls">
<div class="gf-form">
<label class="gf-form-label width-2">{{ $index }}</label>
<label class="gf-form-label width-2">
<a class="pointer" tabindex="1" ng-click="ctrl.onEditorRemoveUrl($index)">
<i class="fa fa-trash" />
</a>
</label>
<label class="gf-form-label width-4">Label: </label>
<input type="text" class="gf-form-input width-16" placeholder="My URL" data-placement="right"
ng-model="url.label" ng-change="ctrl.refresh()">
<label class="gf-form-label width-4">URL: </label>
<input type="text" class="gf-form-input width-c-50" placeholder="https://www.google.es" data-placement="right"
bs-tooltip="'This is the url to be shown when the user clicks on it'" ng-model="url.base_url"
ng-change="ctrl.refresh()">
<info-popover mode="right-normal">
<p>Specify an URL (relative or absolute)</p>
<span>
Use special variables to specify cell values:
<br>
<em>$series_label</em> name of the serie
<br>
<em>$(template.name)</em> name of the templated to be replaced
<br>
<em>$time</em> append &from...&to on the URL
<br>
<em>$series_extra</em> use extraSeries value to use it on URL
</span>
</info-popover>
</div>
<div class="gf-form">
<label class="gf-form-label width-4">Icon: </label>
<input type="text" class="gf-form-input width-12" placeholder="FA Icon" data-placement="right"
bs-tooltip="'The icon shown on URL'" ng-model="url.icon_fa" ng-change="ctrl.refresh()">
<gf-form-switch class="gf-form" label-class="width-8" label="Force lowercase" checked="url.forcelowercase"
on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-6" label="Use Extra Serie Data" checked="url.useExtraSeries"
on-change="ctrl.render()">
</gf-form-switch>
<div class="gf-form" ng-if="url.useExtraSeries == true">
<label class="gf-form-label width-8">Extra Series Index: </label>
<input type="number" class="gf-form-input width-12" placeholder="0" ng-if="url.useExtraSeries == true"
data-placement="right" bs-tooltip="'Fields index to use its value on URL using $series_extra. Use -1 to disable it'" ng-model="url.extraSeries.index"
ng-change="ctrl.refresh()">
<label class="gf-form-label width-9">Type</label>
<div class="gf-form-select-wrapper width-8">
<select class="input-small gf-form-input" ng-model="url.type" ng-options="s for s in ctrl.extraSeriesFormats" ng-change="ctrl.onChangeType(url)"></select>
</div>
<div ng-if="url.type === 'Date'">
<label class="gf-form-label width-12">Extra Series Date Format: </label>
<input type="text" class="gf-form-input width-12" placeholder="YYYY/MM/DD/HH_mm_ss" ng-if="url.useExtraSeries == true"
data-placement="right" bs-tooltip="'Date format to transformat extraSeries'" ng-model="url.extraSeries.format"
ng-change="ctrl.refresh()">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
+120
View File
@@ -0,0 +1,120 @@
<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>$(template.name)</em> name of the templated to be replaced -->
<br>
<em>${__bucket_range}</em> refers to bucket range in the form of '&from=...&to=...'.
<br>
<em>${__graph_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.
</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>
-1
View File
@@ -17,7 +17,6 @@
"links": [
{"name": "Github", "url": "https://github.com/flant/grafana-statusmap"},
{"name": "MIT Licence", "url": "https://github.com/flant/grafana-statusmap/blob/master/LICENSE"}
],
"screenshots": [
{"name": "Panel with light theme", "path": "img/flant-statusmap-panel.png"},
+16 -27
View File
@@ -3,11 +3,9 @@ import $ from 'jquery';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
import {appEvents, contextSrv} from 'app/core/core';
import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
import * as d3 from 'd3';
import * as d3ScaleChromatic from './libs/d3-scale-chromatic/index';
import {StatusmapTooltip} from './tooltip';
import {StatusHeatmapTooltipExtraSeries} from './tooltipextraseries';
import {AnnotationTooltip} from './annotations';
import { Bucket, BucketMatrix } from './statusmap_data';
import { StatusHeatmapCtrl } from './module';
@@ -65,7 +63,6 @@ export class StatusmapRenderer {
panel: any;
$heatmap: any;
tooltip: StatusmapTooltip;
tooltipExtraSeries:StatusHeatmapTooltipExtraSeries;
annotationTooltip: AnnotationTooltip;
heatmap: any;
timeRange: any;
@@ -80,7 +77,6 @@ export class StatusmapRenderer {
// $heatmap is JQuery object, but heatmap is D3
this.$heatmap = this.elem.find('.statusmap-panel');
this.tooltip = new StatusmapTooltip(this.$heatmap, this.scope);
this.tooltipExtraSeries = new StatusHeatmapTooltipExtraSeries(this.$heatmap, this.scope);
this.annotationTooltip = new AnnotationTooltip(this.$heatmap, this.scope);
this.yOffset = 0;
@@ -312,8 +308,10 @@ export class StatusmapRenderer {
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")
.insert("svg",":first-child")
.attr("width", this.width)
.attr("height", this.height);
@@ -428,9 +426,6 @@ export class StatusmapRenderer {
getColorScale(maxValue, minValue = 0) {
let colorScheme = _.find(this.ctrl.colorSchemes, {value: this.panel.color.colorScheme});
// if (!colorScheme) {
//
// }
let colorInterpolator = d3ScaleChromatic[colorScheme.value];
let colorScaleInverted = colorScheme.invert === 'always' ||
(colorScheme.invert === 'dark' && !contextSrv.user.lightTheme);
@@ -537,26 +532,26 @@ export class StatusmapRenderer {
return h;
}
getCardColor(b: Bucket) {
getCardColor(bucket: Bucket) {
if (this.panel.color.mode === 'opacity') {
return this.panel.color.cardColor;
} else if (this.panel.color.mode === 'spectrum') {
return this.colorScale(b.value);
return this.colorScale(bucket.value);
} else if (this.panel.color.mode === 'discrete') {
if (this.panel.seriesFilterIndex != null && this.panel.seriesFilterIndex != -1) {
return this.ctrl.discreteExtraSeries.getBucketColorSingle(d.values[this.panel.seriesFilterIndex]);
return this.ctrl.discreteExtraSeries.getBucketColorSingle(bucket.values[this.panel.seriesFilterIndex]);
} else {
return this.ctrl.discreteExtraSeries.getBucketColor(b.values);
return this.ctrl.discreteExtraSeries.getBucketColor(bucket.values);
}
}
}
getCardOpacity(b: Bucket) {
if (this.panel.nullPointMode === 'as empty' && b.value == null ) {
getCardOpacity(bucket: Bucket) {
if (this.panel.nullPointMode === 'as empty' && bucket.value == null ) {
return 0;
}
if (this.panel.color.mode === 'opacity') {
return this.opacityScale(b.value);
return this.opacityScale(bucket.value);
} else {
return 1;
}
@@ -615,17 +610,10 @@ export class StatusmapRenderer {
onMouseLeave(e) {
appEvents.emit('graph-hover-clear');
this.clearCrosshair();
//annotationTooltip.destroy();
if (e.relatedTarget) {
if (e.relatedTarget.className == "statusmap-tooltip-extraseries graph-tooltip grafana-tooltip" || e.relatedTarget.className == "graph-tooltip-time" ) {
} else {
this.tooltipExtraSeries.destroy();
}
}
this.annotationTooltip.destroy();
this.annotationTooltip.destroy();
}
onMouseMove(event) {
onMouseMove(event: MouseEvent) {
if (!this.heatmap) { return; }
const offset = this.getEventOffset(event);
@@ -646,9 +634,10 @@ export class StatusmapRenderer {
}
}
public onMouseClick(event) {
this.tooltipExtraSeries.show(event)
if (this.ctrl.panel.usingUrl) {
// 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();
}
}
+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}");
});
});
});
});
+176 -31
View File
@@ -7,22 +7,29 @@ import { StatusHeatmapCtrl } from './module';
let TOOLTIP_PADDING_X = 30;
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 {
tooltip: any;
scope: any;
dashboard: any;
panelCtrl: StatusHeatmapCtrl;
panel: any;
heatmapPanel: any;
panelElem: any;
mouseOverBucket: any;
originalFillColor: any;
tooltipWidth: number;
tooltipFrozen: any;
constructor(elem: any, scope: any) {
this.scope = scope;
this.dashboard = scope.ctrl.dashboard;
this.panelCtrl = scope.ctrl;
this.panel = scope.ctrl.panel;
this.heatmapPanel = elem;
this.panelElem = elem;
this.mouseOverBucket = false;
this.originalFillColor = null;
@@ -35,7 +42,7 @@ export class StatusmapTooltip {
if (!this.tooltip) {
this.add();
this.move(e);
this.move(e, this.tooltip);
}
}
@@ -46,13 +53,13 @@ export class StatusmapTooltip {
onMouseMove(e) {
if (!this.panel.tooltip.show) { return; }
this.move(e);
this.move(e, this.tooltip);
}
add() {
this.tooltip = d3.select("body")
.append("div")
.attr("class", "statusmap-tooltip graph-tooltip grafana-tooltip");
.attr("class", "graph-tooltip statusmap-tooltip");
}
destroy() {
@@ -63,13 +70,37 @@ export class StatusmapTooltip {
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; }
// TODO support for shared tooltip mode
if (pos.panelRelY) {
return;
}
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');
@@ -80,36 +111,44 @@ export class StatusmapTooltip {
}
let timestamp = bucket.to;
let name = bucket.yLabel;
let yLabel = bucket.yLabel;
let value = bucket.value;
let values = bucket.values;
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
let time = this.dashboard.formatDate(+timestamp, tooltipTimeFormat);
// TODO create option for this formatting.
let tooltipTimeFormat:string = 'YYYY-MM-DD HH:mm:ss';
let time: Date = this.dashboard.formatDate(+timestamp, tooltipTimeFormat);
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
<div class="statusmap-histogram"></div>`;
// 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 statuses;
let tooltipHtml:string = `<div class="graph-tooltip-time">${time}${tooltipClose}</div>`;
if (this.panel.color.mode === 'discrete') {
let statuses;
if (this.panel.seriesFilterIndex >= 0) {
statuses = this.panelCtrl.discreteExtraSeries.convertValueToTooltips(value);
} else {
statuses = this.panelCtrl.discreteExtraSeries.convertValuesToTooltips(values);
}
let statusesHtml = '';
if (statuses.length === 1) {
statusesHtml = "status:";
} else if (statuses.length > 1) {
statusesHtml = "statuses:";
let statusTitle:string = "status:";
if (statuses.length > 1) {
statusTitle = "statuses:";
}
tooltipHtml += `
<div>
name: <b>${name}</b> <br>
${statusesHtml}
name: <b>${yLabel}</b>
<br>
<span>${statusTitle}</span>
<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>
</div>`;
} else {
@@ -120,7 +159,7 @@ export class StatusmapTooltip {
</div>`;
} else {
tooltipHtml += `<div>
name: <b>${name}}</b> <br>
name: <b>${name}</b> <br>
values:
<ul>
${_.join(_.map(values, v => `<li>${v}</li>`), "")}
@@ -129,14 +168,73 @@ export class StatusmapTooltip {
}
}
// "Ambiguous bucket state: Multiple values!";
tooltipHtml += `<div class="statusmap-histogram"></div>`;
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>`;
}
// Discrete mode errors
if (this.panel.color.mode === 'discrete') {
if (bucket.noColorDefined) {
let badValues = this.panelCtrl.discreteExtraSeries.getNotColoredValues(values);
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.noColorDefined.title}
@@ -145,20 +243,32 @@ export class StatusmapTooltip {
${_.join(_.map(badValues, v => `<li>${v}</li>`), "")}
</ul>
</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) {
if (!this.tooltip) { return; }
// Move tooltip as absolute positioned element.
move(pos, tooltip) {
if (!tooltip) { return; }
let elem = $(this.tooltip.node())[0];
let elem = $(tooltip.node())[0];
let tooltipWidth = elem.clientWidth;
this.tooltipWidth = tooltipWidth;
let tooltipHeight = elem.clientHeight;
let left = pos.pageX + TOOLTIP_PADDING_X;
@@ -172,8 +282,43 @@ export class StatusmapTooltip {
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
}
return this.tooltip
return tooltip
.style("left", left + "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,
};
}
-222
View File
@@ -1,222 +0,0 @@
import d3 from 'd3';
import _ from 'lodash';
import $ from 'jquery';
import { ExtraSeriesFormatValue } from './extra_series_format';
const TOOLTIP_PADDING_X = -50;
const TOOLTIP_PADDING_Y = 5;
export class StatusHeatmapTooltipExtraSeries {
scope: any;
dashboard: any;
panelCtrl: any;
panel: any;
heatmapPanel: any;
mouseOverBucket: any;
originalFillColor: any;
tooltip: any;
constructor(elem: any, scope: any) {
this.scope = scope;
this.dashboard = scope.ctrl.dashboard;
this.panelCtrl = scope.ctrl;
this.panel = scope.ctrl.panel;
this.heatmapPanel = elem;
this.mouseOverBucket = false;
this.originalFillColor = null;
elem.on("mouseover", this.onMouseOver.bind(this));
elem.on("click", this.onMouseClick.bind(this));
}
public onMouseOver(e: Event) {
if (!this.panel.usingUrl || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) {
return;
}
}
public onMouseClick(e: Event) {
if (!this.panel.usingUrl) {
return;
}
this.destroy();
this.add();
}
public add() {
this.tooltip = d3.select("body")
.append("div")
.attr("class", "statusmap-tooltip-extraseries graph-tooltip grafana-tooltip");
}
public destroy() {
if (this.tooltip) {
this.tooltip.remove();
}
this.tooltip = null;
}
public show(pos: any) {
if (!this.panel.usingUrl || !this.tooltip) {
return;
}
// shared tooltip mode
if (pos.panelRelY) {
return;
}
let cardId: any = d3.select(pos.target).attr('cardId');
if (!cardId) {
this.destroy();
return;
}
let card: any = this.panelCtrl.cardsData.cards[cardId];
if (!card) {
this.destroy();
return;
}
if (card.value == null) {
this.destroy();
return;
}
let x: any = card.x;
let y: any = card.y;
let value: any = card.value;
let values: any = card.values;
let tooltipTimeFormat: string = 'YYYY-MM-DD HH:mm:ss';
let time: Date = this.dashboard.formatDate(+x, tooltipTimeFormat);
let tooltipHtml: string = `<div class="graph-tooltip-time">${time}</div>`
if (this.panel.color.mode === 'discrete') {
let statuses: any = "";
if (this.panelCtrl.panel.seriesFilterIndex == -1) {
statuses = this.panelCtrl.discreteExtraSeries.convertValuesToTooltips(values);
} else {
statuses = this.panelCtrl.discreteExtraSeries.convertValueToTooltips(value);
}
tooltipHtml += `
<div>
name: <b>${y}</b>
<div>
<br>
<span>status:</span>
<ul>
${_.join(_.map(statuses, v => `<p style="background-color: ${v.color}; text-align:center" class="discrete-item">${v.tooltip}</p>`), "")}
</ul>
</div>
</div> <br>`;
}
tooltipHtml += `<div class="statusmap-histogram"></div>`;
let urls: any = this.panelCtrl.panel.urls
let rtime: any = this.panelCtrl.retrieveTimeVar();
let curl: any = JSON.parse(JSON.stringify(urls));
for (let i = 0; i < curl.length; i++) {
//Change name var
curl[i].base_url = _.replace(curl[i].base_url, /\$series_label/g, y)
//Set up extra series
if (curl[i].useExtraSeries == true) {
let tf: any = curl[i].extraSeries.format
let vh: any = card.values[curl[i].extraSeries.index]
//let extraSeries: any = this.dashboard.formatDate(+vh, tf)
let series_extra: any = this.formatExtraSeries(vh,tf);
curl[i].base_url = _.replace(curl[i].base_url, /\$series_extra/g, series_extra)
}
//Change time var
curl[i].base_url = _.replace(curl[i].base_url, /\$time/g, rtime)
//Replace vars
curl[i].base_url = this.panelCtrl.renderLink(curl[i].base_url, this.panelCtrl.variableSrv.variables)
}
if (this.panelCtrl.panel.usingUrl == true) {
tooltipHtml += `
<div bs-tooltip='Settings' data-placement="right">
${_.join(_.map(curl, v => `<div ><a href="${v.forcelowercase ? v.base_url.toLowerCase() : v.base_url}" 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 ? v.label : (v.base_url != "" ? _.truncate(v.base_url) : "Empty URL" )}</span><span class="dashlist-star">
<i class="fa fa-${v.icon_fa}"></i>
</span></p> </div></a><div>`), "")}
</div> <br>`;
}
// "Ambiguous bucket state: Multiple values!";
if (!this.panel.useMax && card.multipleValues) {
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.multipleValues.title}</div>`;
}
// Discrete mode errors
if (this.panel.color.mode === 'discrete') {
if (card.noColorDefined) {
let badValues: any = this.panelCtrl.discreteExtraSeries.getNotColoredValues(values);
tooltipHtml += `<div><b>Error:</b> ${this.panelCtrl.dataWarnings.noColorDefined.title}
<br>not colored values:
<ul>
${_.join(_.map(badValues, v => `<li>${v}</li>`), "")}
</ul>
</div>`;
}
}
this.tooltip.html(tooltipHtml);
this.move(pos);
}
public formatExtraSeries (value: any, type: any) {
let extraSeries: any = '';
switch(type) {
case ExtraSeriesFormatValue.Date:
extraSeries = this.dashboard.formatDate(+value, type);
return extraSeries;
case ExtraSeriesFormatValue.Raw:
extraSeries = value;
return extraSeries;
default:
return extraSeries;
}
}
public move(pos: any) {
if (!this.tooltip) {
return;
}
let elem: any = $(this.tooltip.node())[0];
let tooltipWidth = elem.clientWidth;
let tooltipHeight = elem.clientHeight;
let left = pos.pageX + TOOLTIP_PADDING_X;
let top = pos.pageY + TOOLTIP_PADDING_Y;
if (pos.pageX + tooltipWidth + 40 > window.innerWidth) {
left = pos.pageX - tooltipWidth - TOOLTIP_PADDING_X;
}
if (pos.pageY - window.pageYOffset + tooltipHeight + 20 > window.innerHeight) {
top = pos.pageY - tooltipHeight - TOOLTIP_PADDING_Y;
}
return this.tooltip
.style("left", left + "px")
.style("top", top + "px");
}
}