feat: pagination

Continuation of PR #93.
This commit is contained in:
Joaquín Jiménez García
2020-07-13 14:16:37 +03:00
committed by Ivan Mikheykin
parent 4af775ee1f
commit 99ba3cbdd6
25 changed files with 1223 additions and 34 deletions
+2
View File
@@ -102,6 +102,8 @@
width: 70.0rem!important
}
.pagination-buttons{text-align: right;}
.status-heatmap-legend-wrapper {
margin: 0 10px;
+9
View File
@@ -0,0 +1,9 @@
export enum ExtraSeriesFormat {
Date = 'Date',
Raw = 'Raw',
}
export enum ExtraSeriesFormatValue {
Date = 'YYYY/MM/DD/HH_mm_ss',
Raw = '',
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

+22 -2
View File
@@ -1,4 +1,13 @@
<div class="status-heatmap-wrapper">
<div class="clearfix"></div>
<br>
<div class="gf-form" ng-if="ctrl.panel.usingPagination">
<label class="gf-form-label width-9">Items per page</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right"
bs-tooltip="'Number of items to show per page.'" ng-model="ctrl.pageSizeViewer" ng-change="ctrl.changePaginationSize()"
ng-model-onblur>
</div>
<div class="status-heatmap-canvas-wrapper">
<div class="datapoints-warning" ng-if="ctrl.multipleValues || ctrl.noColorDefined || ctrl.noDatapoints">
@@ -7,8 +16,19 @@
<span class="small" ng-if="ctrl.noDatapoints" bs-tooltip="'{{ctrl.dataWarnings.noDatapoints.tip}}'">{{ctrl.dataWarnings.noDatapoints.title}}</span>
</div>
<div class="statusmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
<div class="status-heatmap-panel" ng-dblclick="ctrl.zoomOut()"></div>
</div>
<div class="pagination" ng-if="ctrl.panel.usingPagination">
Showing {{ctrl.firstPageElement}} to {{ctrl.lastPageElement}} of {{ctrl.totalElements}} entries
<div class="pagination-buttons">
<button class="btn btn-inverse" ng-disabled="ctrl.currentPage == 0" ng-click="ctrl.currentPage=ctrl.currentPage-1; ctrl.render()">
Previous
</button>
{{ctrl.currentPage+1}}/{{ctrl.numberOfPages}}
<button class="btn btn-inverse" ng-disabled="ctrl.currentPage >= ctrl.cardsData.targets.length/ctrl.pageSizeViewer - 1 || ctrl.numberOfPages == 0" ng-click="ctrl.currentPage=ctrl.currentPage+1; ctrl.render()">
Next
</button>
</div>
</div>
<div class="status-heatmap-legend-wrapper" ng-if="ctrl.panel.legend.show">
<status-heatmap-legend></status-heatmap-legend>
+99 -6
View File
@@ -78,6 +78,10 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
colorSchemes: any = [];
unitFormats: any;
cardsDataComplete: any;
cardsDataLabelsComplete: any;
ticksWhenPaginating: [];
dataWarnings: {[warningId: string]: {title: string, tip: string}} = {};
multipleValues: boolean;
noColorDefined: boolean;
@@ -88,6 +92,13 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
annotations: object[] = [];
annotationsPromise: any;
pageSizeViewer: number = 15;
currentPage: number = 0;
numberOfPages: number = 1;
totalElements: number = 0;
firstPageElement: number = 1;
lastPageElement: number = 5;
panelDefaults: any = {
// datasource name, null = default datasource
datasource: null,
@@ -132,7 +143,11 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
highlightCards: true,
useMax: true,
seriesFilterIndex: -1
seriesFilterIndex: -1,
usingPagination: false,
pageSize: 15,
allowAllElements: false,
availableValues: []
};
/** @ngInject */
@@ -148,6 +163,11 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
migratePanelConfig(this.panel);
_.defaultsDeep(this.panel, this.panelDefaults);
if (this.panel.usingPagination) {
this.setPaginationSize(this.panel.pageSize);
this.setCurrentPage(0);
}
this.opacityScales = opacityScales;
this.colorModes = colorModes;
this.colorSchemes = colorSchemes;
@@ -179,7 +199,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.annotations = [];
this.annotationsSrv = annotationsSrv;
this.timeSrv = timeSrv;
this.events.on(PanelEvents.render, this.onRender.bind(this));
@@ -199,6 +219,68 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.renderingCompleted();
}
changeDefaultPaginationSize(defaultPageSize: number): void {
this.pageSizeViewer = defaultPageSize;
this.render();
this.refresh();
}
changePaginationSize(): void {
if (this.pageSizeViewer <= 0) {
this.pageSizeViewer = 1;
}
this.currentPage = 0;
this.render();
this.refresh();
}
getPaginationSize(): number {
return this.pageSizeViewer;
}
setPaginationSize(pageSize: number): void {
this.pageSizeViewer = pageSize;
}
getCurrentPage(): number {
return this.currentPage;
}
setCurrentPage(currentPage: number): void {
this.currentPage = currentPage;
}
getNumberOfPages(): number {
return this.numberOfPages;
}
getTotalElements(): number {
return this.totalElements;
}
setTotalElements(totalElements: number): void {
this.totalElements = totalElements;
}
getFirstPageElement(): number {
return this.firstPageElement;
}
setFirstPageElement(firstPageElement: number): void {
this.firstPageElement = firstPageElement;
}
getLastPageElement(): number {
return this.lastPageElement;
}
setLastPageElement(lastPageElement: number): void {
this.lastPageElement = lastPageElement;
}
// getChartWidth returns an approximation of chart canvas width or
// a saved value calculated during a render.
getChartWidth():number {
@@ -323,6 +405,13 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.bucketMatrix = this.convertDataToBuckets(dataList, this.range.from.valueOf(), this.range.to.valueOf(), this.intervalMs, true);
this.noDatapoints = this.bucketMatrix.noDatapoints;
if (this.panel.usingPagination && this.cardsData.targets) {
this.numberOfPages = Math.ceil(this.cardsData.targets.length/this.pageSizeViewer);
this.totalElements = this.cardsData.targets.length;
} else {
this.setPaginationSize(this.cardsData.targets.length);
}
if (this.annotationsPromise) {
this.annotationsPromise.then(
(result: { alertState: any; annotations: any }) => {
@@ -338,7 +427,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
() => {
this.loading = false;
this.annotations = [];
this.render();
this.render(); // this.render(this.data);???
}
);
} else {
@@ -354,6 +443,10 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
this.unitFormats = kbn.getUnitFormats();
}
paginate() {
this.refresh();
}
// onRender will be called before StatusmapRenderer.onRender.
// Decide if warning should be displayed over cards.
onRender() {
@@ -453,7 +546,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
bucketMatrix.rangeMs = to - from;
bucketMatrix.intervalMs = intervalMs;
if (!data || data.length == 0) {
if (!data || data.length == 0) {
// Mimic heatmap and graph 'no data' labels.
bucketMatrix.targets = [
"1.0", "0.0", "-1.0"
@@ -469,7 +562,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
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);
@@ -577,7 +670,7 @@ class StatusHeatmapCtrl extends MetricsPanelCtrl {
});
//console.log ("bucketMatrix: ", bucketMatrix);
// Put values into buckets.
bucketMatrix.minValue = Number.MAX_VALUE;
bucketMatrix.maxValue = Number.MIN_SAFE_INTEGER;
+115
View File
@@ -216,4 +216,119 @@
</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">Pagination options</h5>
<div class="gf-form">
<gf-form-switch class="gf-form" label="Paginate elements" label-class="width-12"
checked="ctrl.panel.usingPagination" on-change="ctrl.paginate()"></gf-form-switch>
</div>
<div class="gf-form" ng-if="ctrl.panel.usingPagination">
<label class="gf-form-label width-9">Items per Page by default</label>
<input type="number" class="gf-form-input width-5" placeholder="2" data-placement="right"
bs-tooltip="Number of items to show by default" ng-model="ctrl.panel.pageSize" ng-change="ctrl.changeDefaultPaginationSize(ctrl.panel.pageSize)"
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>
+105 -8
View File
@@ -32,7 +32,7 @@ class Statusmap {
bucketMatrix: BucketMatrix;
timeRange: {from: number, to: number} = {from:0, to:0};
constructor() {
}
@@ -133,7 +133,11 @@ export class StatusmapRenderer {
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) {
height -= this.panel.legend.show ? 140 : 10; // bottom padding and space for legend. Change margin in .status-heatmap-color-legend !
} else {
height -= this.panel.legend.show ? 50 : 10; // bottom padding and space for legend. Change margin in .status-heatmap-color-legend !
}
this.$heatmap.css('height', height + 'px');
@@ -210,7 +214,9 @@ export class StatusmapRenderer {
// divide chart height by ticks for cards drawing
getYScale(ticks: any[]) {
let range:any[] = [];
let step = this.chartHeight / ticks.length;
//let step = this.chartHeight / ticks.length;
console.log('GETYSCALE - RENDERING', this.ctrl.getPaginationSize());
let step = this.chartHeight / this.ctrl.getPaginationSize();
// svg has y=0 on the top, so top card should have a minimal value in range
range.push(step);
for (let i = 1; i < ticks.length; i++) {
@@ -224,7 +230,9 @@ export class StatusmapRenderer {
// divide chart height by ticks with offset for ticks drawing
getYAxisScale(ticks: any[]) {
let range:any[] = [];
let step = this.chartHeight / ticks.length;
//let step = this.chartHeight / ticks.length;
console.log('GETYAXISSCALE - RENDERING', this.ctrl.getPaginationSize());
let step = this.chartHeight / this.ctrl.getPaginationSize();
// svg has y=0 on the top, so top tick should have a minimal value in range
range.push(this.yOffset);
for (let i = 1; i < ticks.length; i++) {
@@ -236,7 +244,25 @@ export class StatusmapRenderer {
}
addYAxis() {
let ticks = this.bucketMatrix.targets;
let ticks;
if(this.ctrl.ticksWhenPaginating !== undefined && this.ctrl.panel.usingPagination) {
ticks = _.uniq(_.map(this.ctrl.ticksWhenPaginating));
} else {
ticks = this.bucketMatrix.targets;
//ticks = _.uniq(_.map(this.data, d => d.target));
}
// // Set default Y min and max if no data
// if (_.isEmpty(this.data)) {
// ticks = [''];
// }
//
// // TODO
// // New version!
// let ticks = this.bucketMatrix.targets;
if (this.panel.yAxisSort == 'a → z') {
ticks.sort((a, b) => a.localeCompare(b, 'en', {ignorePunctuation: false, numeric: true}));
@@ -326,9 +352,17 @@ export class StatusmapRenderer {
// calculate yOffset for YAxis
this.yGridSize = this.chartHeight;
if (this.bucketMatrix.targets.length > 0) {
this.yGridSize = Math.floor(this.chartHeight / this.bucketMatrix.targets.length);
if (this.ctrl.panel.usingPagination) {
// Pagination mode overrides count of rows.
//this.yGridSize = Math.floor(this.chartHeight / this.ctrl.ticksWhenPaginating.length);
console.log('addHeatmapCanvas - rendering',this.ctrl.getPaginationSize());
this.yGridSize = Math.floor(this.chartHeight / this.ctrl.getPaginationSize());
} else {
if (this.bucketMatrix.targets.length > 0) {
this.yGridSize = Math.floor(this.chartHeight / this.bucketMatrix.targets.length);
}
}
this.cardHeight = this.yGridSize ? this.yGridSize - this.cardVSpacing : 0;
this.yOffset = this.cardHeight / 2;
@@ -501,7 +535,6 @@ export class StatusmapRenderer {
// Top y for card.
// yScale gives ???
//
getCardY(b: Bucket) {
return this.yScale(b.yLabel) + this.chartTop - this.cardHeight - this.cardVSpacing/2;
}
@@ -753,6 +786,67 @@ export class StatusmapRenderer {
if (!this.bucketMatrix || !this.setElementHeight()) {
return;
} else {
// TODO pagination!
this.ctrl.cardsDataComplete = this.ctrl.bucketMatrix.buckets;
if(this.ctrl.panel.usingPagination) {
if (!this.bucketMatrix.targets) {
return;
}
this.ctrl.setFirstPageElement((this.ctrl.getCurrentPage()*this.ctrl.getPaginationSize())+1);
let elems = this.ctrl.getTotalElements();
console.log('total elems in add heatmap canvas', elems);
if (elems < this.ctrl.getFirstPageElement()) {
this.ctrl.setCurrentPage(0);
this.render();
}
if (this.ctrl.getPaginationSize() >= this.ctrl.getTotalElements()) {
this.ctrl.setLastPageElement(this.ctrl.getTotalElements());
} else {
if ((this.ctrl.getCurrentPage()+1) === this.ctrl.getNumberOfPages() && (this.ctrl.getCurrentPage()>0)) {
this.ctrl.setLastPageElement(this.ctrl.getTotalElements());
} else {
this.ctrl.setLastPageElement((this.ctrl.getCurrentPage() * this.ctrl.getPaginationSize())+this.ctrl.getPaginationSize());
}
}
let cardsList = this.ctrl.bucketMatrix.targets.slice(this.ctrl.getPaginationSize()*this.ctrl.getCurrentPage(),
(this.ctrl.getPaginationSize()*this.ctrl.getCurrentPage())+this.ctrl.getPaginationSize());
let cardsToShow = [];
let labelsToShow = [];
// Rewrite for bucket matrix.
for (let i = 0; i < this.cardsData.cards.length; i++) {
const card = this.cardsData.cards[i];
for (let j = 0; j < cardsList.length; j++) {
const value = cardsList[j];
if (card.y === value) {
cardsToShow.push(card);
labelsToShow.push(value);
}
}
}
const labelsToShowClean = [...new Set(labelsToShow)];
this.cardsData.cards = cardsToShow.slice();
this.ctrl.ticksWhenPaginating = labelsToShowClean;
cardsToShow = undefined;
} else {
const pageSize = this.ctrl.bucketMatrix.targets.length
this.ctrl.getPaginationSize(pageSize);
this.ctrl.ticksWhenPaginating = undefined;
}
}
// Draw default axes and return if no data
@@ -767,6 +861,9 @@ export class StatusmapRenderer {
this.scope.chartHeight = this.chartHeight;
this.scope.chartWidth = this.chartWidth;
this.scope.chartTop = this.chartTop;
// TODO pagination. Why this needed?
//this.ctrl.cardsData.cards = this.ctrl.cardsDataComplete.slice();
}
_renderAnnotations() {
+228
View File
@@ -0,0 +1,228 @@
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;
if (this.panel.usingPagination) {
card = this.panelCtrl.cardsDataComplete[cardId];
} else {
card = 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");
}
}