mirror of
https://github.com/timberjoegithub/grafana-statusmap.git
synced 2026-07-23 16:13:09 +00:00
Actually, custom label are not allowed, it directly uses the provided by the query result as the target/alias This feature allows to: - Split YLabel to store its partials and reuse them on link and extra tooltip field - Override YLabel with splitted and custom values - Added Extra Tooltip to show metadata to the user on each serie
337 lines
10 KiB
TypeScript
337 lines
10 KiB
TypeScript
import d3 from 'd3';
|
||
import $ from 'jquery';
|
||
import _ from 'lodash';
|
||
|
||
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;
|
||
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.panelElem = elem;
|
||
this.mouseOverBucket = false;
|
||
this.originalFillColor = null;
|
||
|
||
elem.on("mouseover", this.onMouseOver.bind(this));
|
||
elem.on("mouseleave", this.onMouseLeave.bind(this));
|
||
}
|
||
|
||
onMouseOver(e) {
|
||
if (!this.panel.tooltip.show || !this.scope.ctrl.data || _.isEmpty(this.scope.ctrl.data)) { return; }
|
||
|
||
if (!this.tooltip) {
|
||
this.add();
|
||
this.move(e, this.tooltip);
|
||
}
|
||
}
|
||
|
||
onMouseLeave() {
|
||
this.destroy();
|
||
}
|
||
|
||
onMouseMove(e) {
|
||
if (!this.panel.tooltip.show) { return; }
|
||
|
||
this.move(e, this.tooltip);
|
||
}
|
||
|
||
add() {
|
||
this.tooltip = d3.select("body")
|
||
.append("div")
|
||
.attr("class", "graph-tooltip statusmap-tooltip");
|
||
}
|
||
|
||
destroy() {
|
||
if (this.tooltip) {
|
||
this.tooltip.remove();
|
||
}
|
||
|
||
this.tooltip = null;
|
||
}
|
||
|
||
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 tooltip’s 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();
|
||
return;
|
||
}
|
||
|
||
let timestamp = bucket.to;
|
||
let yLabel = bucket.yLabel;
|
||
let pLabels = bucket.pLabels;
|
||
let value = bucket.value;
|
||
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 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 statusTitle:string = "status:";
|
||
if (statuses.length > 1) {
|
||
statusTitle = "statuses:";
|
||
}
|
||
tooltipHtml += `
|
||
<div>
|
||
name: <b>${yLabel}</b>
|
||
<br>
|
||
<span>${statusTitle}</span>
|
||
<ul>
|
||
${_.join(_.map(statuses, v => `<li style="background-color: ${v.color}; text-align:center" class="discrete-item">${v.tooltip}</li>`), "")}
|
||
</ul>
|
||
</div>`;
|
||
} else {
|
||
if (values.length === 1) {
|
||
tooltipHtml += `<div>
|
||
name: <b>${yLabel}</b> <br>
|
||
value: <b>${value}</b> <br>
|
||
</div>`;
|
||
} else {
|
||
tooltipHtml += `<div>
|
||
name: <b>${yLabel}</b> <br>
|
||
values:
|
||
<ul>
|
||
${_.join(_.map(values, v => `<li>${v}</li>`), "")}
|
||
</ul>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
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};
|
||
scopedVars[`__y_label_trim`] = {value: yLabel.trim()};
|
||
// Grafana 7.0 compatible
|
||
scopedVars[`__url_time_range`] = {value: this.panelCtrl.retrieveTimeVar()};
|
||
|
||
//New vars based on partialLabels:
|
||
for (let i in pLabels) {
|
||
scopedVars[`__y_label_${i}`] = {value: pLabels[i]};
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
if (this.panel.tooltip.showCustomContent) {
|
||
let customContent: string = this.panelCtrl.templateSrv.replace(this.panel.tooltip.customContent, scopedVars)
|
||
tooltipHtml += `<div>${customContent}</div>`
|
||
}
|
||
|
||
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}
|
||
<br>not colored values:
|
||
<ul>
|
||
${_.join(_.map(badValues, v => `<li>${v}</li>`), "")}
|
||
</ul>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
tooltip.html(tooltipHtml);
|
||
|
||
// Assign mouse event handlers for "frozen" tooltip.
|
||
if (frozen) {
|
||
// Stop propagation mouse events up to parents to allow interaction with frozen tooltip’s 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.
|
||
move(pos, tooltip) {
|
||
if (!tooltip) { return; }
|
||
|
||
let elem = $(tooltip.node())[0];
|
||
let tooltipWidth = elem.clientWidth;
|
||
this.tooltipWidth = tooltipWidth;
|
||
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 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 tooltip’s 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 tooltip’s root element is appended next to panel’s svg element,
|
||
// so top property is adjusted to move tooltip’s 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");
|
||
}
|
||
|
||
|
||
}
|