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
+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");
}
}