Files
mikromon/static/js/app.js
Mateusz Gruszczyński e8f6c4c609 push
2026-03-05 15:53:33 +01:00

204 lines
6.0 KiB
JavaScript

(function(){
function ringPush(arr, item, max){
arr.push(item);
if(arr.length > max) arr.splice(0, arr.length-max);
}
function ensureLibs(){
return new Promise((resolve)=>{
const tick=()=>{ if(window.io && window.Chart) return resolve(); setTimeout(tick,50); };
tick();
});
}
const charts=new Map();
const seriesData=new Map(); // widgetId -> {name: [{x,y}]}
const seriesUnit=new Map(); // widgetId -> {name: unit}
const maxPoints=900;
function isNum(v){ return typeof v==='number' && Number.isFinite(v); }
function scale(v, unit){
if(!isNum(v)) return {v, u:''};
const u=(unit||'').toLowerCase();
if(u==='%' || u==='pct' || u==='percent') return {v, u:'%'};
if(u==='bps' || u==='bit/s' || u==='bits/s'){
const steps=['bps','Kbps','Mbps','Gbps','Tbps'];
let i=0, x=v;
while(Math.abs(x)>=1000 && i<steps.length-1){ x/=1000; i++; }
return {v:x, u:steps[i]};
}
if(u==='b/s' || u==='bytes/s' || u==='byte/s'){
const steps=['B/s','KiB/s','MiB/s','GiB/s','TiB/s'];
let i=0, x=v;
while(Math.abs(x)>=1024 && i<steps.length-1){ x/=1024; i++; }
return {v:x, u:steps[i]};
}
if(u==='b' || u==='bytes'){
const steps=['B','KiB','MiB','GiB','TiB'];
let i=0, x=v;
while(Math.abs(x)>=1024 && i<steps.length-1){ x/=1024; i++; }
return {v:x, u:steps[i]};
}
// fallback
return {v, u:unit||''};
}
function fmt(v, unit){
if(!isNum(v)) return '';
const s=scale(v, unit);
const a=Math.abs(s.v);
const n = a>=100 ? s.v.toFixed(0) : a>=10 ? s.v.toFixed(1) : s.v.toFixed(2);
return n + (s.u ? ' '+s.u : '');
}
function hash32(str){
let h=2166136261;
for(let i=0;i<str.length;i++){ h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
return h>>>0;
}
function colorFor(name){
const hue = (hash32(String(name)) % 360);
return {
border:`hsl(${hue} 70% 45%)`,
fill:`hsl(${hue} 70% 45% / 0.20)`
};
}
function upsertChart(widgetId){
const el=document.getElementById('chart-'+widgetId);
if(!el) return null;
if(charts.has(widgetId)) return charts.get(widgetId);
const chart=new Chart(el.getContext('2d'),{
type:'line',
data:{datasets:[]},
options:{
animation:false,
parsing:false,
normalized:true,
responsive:true,
maintainAspectRatio:false,
interaction:{mode:'nearest', intersect:false},
scales:{
x:{type:'time', time:{unit:'minute'}},
y:{
beginAtZero:false,
ticks:{
callback:(tick)=>{
// jeśli w widżecie jest 1 unit -> formatuj, jeśli różne -> surowo
const uMap = seriesUnit.get(widgetId) || {};
const units = Object.values(uMap).filter(Boolean);
const uniq = [...new Set(units)];
const u = uniq.length===1 ? uniq[0] : '';
return u ? fmt(Number(tick), u) : tick;
}
}
}
},
plugins:{
legend:{display:true},
tooltip:{
callbacks:{
label:(ctx)=>{
const name=ctx.dataset.label||'';
const y=ctx.parsed && isNum(ctx.parsed.y) ? ctx.parsed.y : null;
const u=(seriesUnit.get(widgetId)||{})[name] || '';
return name+': '+(y===null?'':fmt(y,u));
}
}
}
},
elements:{point:{radius:0}}
}
});
charts.set(widgetId, chart);
seriesData.set(widgetId, {});
seriesUnit.set(widgetId, {});
return chart;
}
function setMeta(widgetId, txt){
const m=document.getElementById('meta-'+widgetId);
if(m) m.textContent=txt;
}
function escapeHtml(s){
return String(s)
.replaceAll('&','&amp;')
.replaceAll('<','&lt;')
.replaceAll('>','&gt;')
.replaceAll('"','&quot;')
.replaceAll("'",'&#039;');
}
function handleTable(msg){
const tbl=document.querySelector("table[data-table-widget='"+msg.widget_id+"']");
if(!tbl) return;
const thead=tbl.querySelector('thead');
const tbody=tbl.querySelector('tbody');
const cols=msg.columns||[];
thead.innerHTML='<tr>'+cols.map(c=>'<th>'+escapeHtml(c)+'</th>').join('')+'</tr>';
tbody.innerHTML=(msg.rows||[]).map(r=>'<tr>'+cols.map(c=>'<td>'+escapeHtml(r[c] ?? '')+'</td>').join('')+'</tr>').join('');
}
function handleTimeseries(msg){
const chart=upsertChart(msg.widget_id);
if(!chart) return;
const t=new Date(msg.ts);
const store=seriesData.get(msg.widget_id) || {};
const uMap=seriesUnit.get(msg.widget_id) || {};
for(const p of (msg.points||[])){
if(p.value===null || p.value===undefined) continue;
const s=p.series || 'value';
if(!store[s]) store[s]=[];
ringPush(store[s], {x:t, y:Number(p.value)}, maxPoints);
if(p.unit) uMap[s]=String(p.unit);
}
seriesData.set(msg.widget_id, store);
seriesUnit.set(msg.widget_id, uMap);
const names = Object.keys(store);
chart.data.datasets = names.map(name=>{
const c=colorFor(name);
return {
label:name,
data:store[name],
borderWidth:2,
pointRadius:0,
tension:0.15,
fill:true,
borderColor:c.border,
backgroundColor:c.fill
};
});
chart.update('none');
setMeta(msg.widget_id, 'Updated: '+t.toLocaleTimeString());
}
async function main(){
if(!window.MIKROMON || !window.MIKROMON.dashboardId) return;
await ensureLibs();
const socket=io({transports:['polling'], upgrade:false});
socket.on('connect', ()=>{
socket.emit('join_dashboard', {dashboard_id: window.MIKROMON.dashboardId, public_token: window.MIKROMON.publicToken});
});
socket.on('metric', (msg)=>{
if(!msg || !msg.widget_id) return;
if(msg.type==='table') return handleTable(msg);
return handleTimeseries(msg);
});
}
document.addEventListener('DOMContentLoaded', main);
})();