Skip to main content

Grafana gapit-htmlgraphics-panel — полный cheatsheet


1. htmlNode вместо document

Плагин рендерит HTML в изолированный Shadow DOM. document.getElementById не найдёт ничего — только htmlNode.getElementById.

// ❌
var el = document.getElementById('root');
// ✅
var el = htmlNode.getElementById('root');

Это же касается querySelectorAll, querySelector и любого поиска по DOM.


2. addEventListener только через htmlNode

Inline обработчики onmouseenter="..." не работают в Shadow DOM. Только через JS после рендера:

root.innerHTML = '...';  // сначала рендер

// потом вешаем события
var card = htmlNode.getElementById('my-card');
card.addEventListener('mouseenter', function() { ... });
card.addEventListener('mouseleave', function() { ... });

3. dynamicData: true — обязательно

По умолчанию dynamicData: falsedata в onRender равна undefined.

Включить через Import/export → Panel options:

"dynamicData": true

4. Структура data.series

data.series[N]         // N-й фрейм
data.series[N].length // количество строк — использовать именно это
data.series[N].fields // массив колонок
data.series[N].refId // идентификатор query: "A", "B", "C"...

Поле:

field.name    // имя колонки
field.type // "number", "string", "time"
field.values // данные — Array или Buffer
field.labels // теги InfluxDB — важно! (см. п.5)

5. Три способа как приходят данные из InfluxDB

Способ 1 — поле как обычная колонка (после pivot или map):

// "hop" labels={} v[0]="inter"
var hopF = sr.fields.find(function(f){ return f.name === 'hop'; });
var hop = val(hopF, 0);

Способ 2 — теги в labels поля (после group + last без pivot):

// "_value" labels={"hop":"inter","status":"idle"} v[0]=1
var valueF = sr.fields.find(function(f){ return f.name === '_value'; });
var labels = valueF.labels || {};
var hop = labels.hop;
var status = labels.status;

Способ 3 — pivot разворачивает _field в колонки:

|> pivot(rowKey: ["_time", "hop"], columnKey: ["_field"], valueColumn: "_value")

Тогда disk_pct, disk_used, mem_used приходят как отдельные поля.


6. Универсальное чтение значения (Array vs Buffer)

InfluxDB иногда возвращает values как Buffer с .get(i), иногда как обычный массив. Всегда использовать обёртку:

function val(field, i) {
if (!field) return null;
var v = field.values;
if (typeof v.get === 'function') return v.get(i);
return v[i];
}

7. Несколько queries — разделять по refId

data.series.forEach(function(sr) {
if (sr.refId === 'A') { /* heartbeat */ }
if (sr.refId === 'B') { /* события */ }
if (sr.refId === 'C') { /* здоровье */ }
});

Все series всех queries идут в один data.series массив подряд.


8. group() + last() + pivot — правильный паттерн для "последнее по каждому хопу"

from(bucket: "backupserver")
|> range(start: -10m)
|> filter(fn: (r) => r._measurement == "pipeline_health")
|> group(columns: ["hop", "_field"])
|> last()
|> group(columns: ["hop"])
|> pivot(rowKey: ["_time", "hop"], columnKey: ["_field"], valueColumn: "_value")
|> group()

Без финального group() — каждый хоп отдельный series. С финальным group() — все хопы в одном series (итерировать по sr.length).


9. CSS анимации — через document.head

В Shadow DOM нельзя добавить <style> тег напрямую в HTML строку для keyframe анимаций. Нужно через JS:

var styleEl = document.getElementById('my-style');
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'my-style';
styleEl.textContent = '@keyframes ping { 75%,100% { transform:scale(2);opacity:0 } }';
document.head.appendChild(styleEl);
}

Проверка if (!styleEl) важна — иначе при каждом рефреше добавляется новый тег.


10. Tooltip — позиционирование

Tooltip внутри карточки с position:absolute обрезается если у родителя overflow:hidden. Правила:

// Карточка — overflow:hidden (для border-radius)
// Внешний контейнер хопа — overflow:visible
// Корневой div — overflow:visible

// Tooltip вверх (не перекрывается легендой внизу):
'position:absolute;bottom:calc(100% + 8px);left:0;z-index:9999'

// Tooltip вниз:
'position:absolute;top:calc(100% + 8px);left:0;z-index:9999'

11. onInit — очистить от дефолтного кода

Дефолтный onInit ищет htmlgraphics-text и рендерит "Random text". Полностью очистить если не используется.


12. centerAlignContent — выключить для списков и карточек

"centerAlignContent": false

При true контент центрируется вертикально — ломает layout карточек.


13. Кириллица в именах полей InfluxDB

Работает но ненадёжно при поиске через f.name === '...'. Лучше использовать indexOf:

fields.find(function(f){ return f.name.indexOf('данных') !== -1; })

Или сразу называть поля латиницей snake_case в Flux запросе.


14. series.length vs field.values.length

// ✅ правильно
var count = sr.length;

// ❌ может не совпадать
var count = sr.fields[0].values.length;

15. Диагностический шаблон

var root = htmlNode.getElementById('root');
if (!root) return;

var out = '<pre style="font-size:10px;color:#aaa;padding:8px;white-space:pre-wrap">';
out += 'browser now: ' + new Date().toISOString() + '\n';
out += 'series total: ' + data.series.length + '\n\n';

data.series.forEach(function(sr, si) {
out += '=== [' + si + '] refId=' + sr.refId + ' len=' + sr.length + ' ===\n';
sr.fields.forEach(function(f) {
var v0 = '?', vl = '?';
try {
v0 = typeof f.values.get==='function' ? f.values.get(0) : f.values[0];
vl = typeof f.values.get==='function' ? f.values.get(sr.length-1) : f.values[sr.length-1];
} catch(e){}
out += ' "' + f.name + '"'
+ ' labels=' + JSON.stringify(f.labels||{})
+ ' v[0]=' + JSON.stringify(v0)
+ ' v[last]=' + JSON.stringify(vl)
+ '\n';
});
out += '\n';
});

out += '</pre>';
root.innerHTML = out;

16. Порядок отладки при пустом тайле

  1. Вставить минимальный тест — убедиться что onRender работает:

    var g = htmlNode.getElementById('root');
    if (g) g.innerHTML = '<div style="color:red">WORKS</div>';
  2. Если нет — проверить HTML, что элемент с нужным id существует.

  3. Если да — вставить диагностический шаблон (п.15).

  4. Если data undefined — включить dynamicData (п.3).

  5. Сверить имена полей из диагностики с именами в коде.

  6. Проверить refId — возможно данные в другом series.

  7. Если теги не видны как поля — читать из field.labels (п.5).


17. Адаптивность тёмная/светлая тема

var isDark = theme.isDark;
var c = {
text: isDark ? '#fafafa' : '#09090b',
textMuted: isDark ? '#a1a1aa' : '#52525b',
textFaint: isDark ? '#71717a' : '#a1a1aa',
cardBg: isDark ? '#27272a' : '#ffffff',
cardBorder: isDark ? '#3f3f46' : '#e4e4e7',
trackBg: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
};

Акцентные цвета (#22c55e, #ef4444, #3b82f6, #f59e0b) — одинаковые в обеих темах, достаточно насыщены для обоих фонов.


18. Inline стили вместо CSS классов

В Shadow DOM CSS классы из секции CSS плагина работают, но inline стили надёжнее при динамической генерации HTML через JS — не зависят от специфичности и порядка загрузки.


19. Root CSS — для шрифтов и глобальных стилей

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
html, body { margin: 0; padding: 0; width: 100%; }

Root CSS загружается вне Shadow DOM — единственное место где работают @import и шрифты.