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: false — data в 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. Порядок отладки при пустом тайле
-
Вставить минимальный тест — убедиться что onRender работает:
var g = htmlNode.getElementById('root');
if (g) g.innerHTML = '<div style="color:red">WORKS</div>'; -
Если нет — проверить HTML, что элемент с нужным id существует.
-
Если да — вставить диагностический шаблон (п.15).
-
Если
data undefined— включить dynamicData (п.3). -
Сверить имена полей из диагностики с именами в коде.
-
Проверить
refId— возможно данные в другом series. -
Если теги не видны как поля — читать из
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 и шрифты.