248 lines
7.5 KiB
JavaScript
248 lines
7.5 KiB
JavaScript
(function(){
|
|
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();
|
|
const seriesUnit=new Map();
|
|
const selectedRange=new Map();
|
|
const maxWindowMs=10*60*60*1000;
|
|
const rangeMap={
|
|
'1m': 1*60*1000,
|
|
'10m': 10*60*1000,
|
|
'1h': 60*60*1000,
|
|
'3h': 3*60*60*1000,
|
|
'10h': 10*60*60*1000,
|
|
};
|
|
|
|
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]};
|
|
}
|
|
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 getRangeMs(widgetId){
|
|
return rangeMap[selectedRange.get(widgetId) || '1h'] || rangeMap['1h'];
|
|
}
|
|
|
|
function pruneStore(store, latestTs){
|
|
const minTs=latestTs-maxWindowMs;
|
|
for(const name of Object.keys(store)){
|
|
store[name]=store[name].filter(p=>p.x.getTime()>=minTs);
|
|
}
|
|
}
|
|
|
|
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)=>{
|
|
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, {});
|
|
if(!selectedRange.has(widgetId)) selectedRange.set(widgetId, '1h');
|
|
return chart;
|
|
}
|
|
|
|
function setMeta(widgetId, txt){
|
|
const m=document.getElementById('meta-'+widgetId);
|
|
if(m) m.textContent=txt;
|
|
}
|
|
|
|
function escapeHtml(s){
|
|
return String(s)
|
|
.replaceAll('&','&')
|
|
.replaceAll('<','<')
|
|
.replaceAll('>','>')
|
|
.replaceAll('"','"')
|
|
.replaceAll("'",''');
|
|
}
|
|
|
|
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 renderChart(widgetId, latestTs){
|
|
const chart=upsertChart(widgetId);
|
|
if(!chart) return;
|
|
const store=seriesData.get(widgetId) || {};
|
|
const rangeMs=getRangeMs(widgetId);
|
|
const minTs=latestTs-rangeMs;
|
|
|
|
chart.data.datasets = Object.keys(store).map(name=>{
|
|
const c=colorFor(name);
|
|
const data=(store[name]||[]).filter(p=>p.x.getTime()>=minTs);
|
|
return {
|
|
label:name,
|
|
data,
|
|
borderWidth:2,
|
|
pointRadius:0,
|
|
tension:0.15,
|
|
fill:true,
|
|
borderColor:c.border,
|
|
backgroundColor:c.fill
|
|
};
|
|
});
|
|
|
|
chart.options.scales.x.min=minTs;
|
|
chart.options.scales.x.max=latestTs;
|
|
chart.update('none');
|
|
}
|
|
|
|
function handleTimeseries(msg){
|
|
const t=new Date(msg.ts);
|
|
const widgetId=msg.widget_id;
|
|
upsertChart(widgetId);
|
|
|
|
const store=seriesData.get(widgetId) || {};
|
|
const uMap=seriesUnit.get(widgetId) || {};
|
|
|
|
for(const p of (msg.points||[])){
|
|
if(p.value===null || p.value===undefined) continue;
|
|
const s=p.series || 'value';
|
|
if(!store[s]) store[s]=[];
|
|
store[s].push({x:t, y:Number(p.value)});
|
|
if(p.unit) uMap[s]=String(p.unit);
|
|
}
|
|
|
|
pruneStore(store, t.getTime());
|
|
seriesData.set(widgetId, store);
|
|
seriesUnit.set(widgetId, uMap);
|
|
renderChart(widgetId, t.getTime());
|
|
setMeta(widgetId, 'Updated: '+t.toLocaleTimeString()+' · Range: '+(selectedRange.get(widgetId)||'1h'));
|
|
}
|
|
|
|
function initRangeSelectors(){
|
|
document.querySelectorAll('[data-range-widget]').forEach((el)=>{
|
|
const widgetId=Number(el.getAttribute('data-range-widget'));
|
|
selectedRange.set(widgetId, el.value || '1h');
|
|
el.addEventListener('change', ()=>{
|
|
selectedRange.set(widgetId, el.value || '1h');
|
|
const store=seriesData.get(widgetId) || {};
|
|
let latestTs=Date.now();
|
|
for(const points of Object.values(store)){
|
|
if(points.length){
|
|
const ts=points[points.length-1].x.getTime();
|
|
if(ts>latestTs) latestTs=ts;
|
|
}
|
|
}
|
|
renderChart(widgetId, latestTs);
|
|
setMeta(widgetId, 'Updated: '+new Date(latestTs).toLocaleTimeString()+' · Range: '+(selectedRange.get(widgetId)||'1h'));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function main(){
|
|
if(!window.MIKROMON || !window.MIKROMON.dashboardId) return;
|
|
await ensureLibs();
|
|
initRangeSelectors();
|
|
|
|
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);
|
|
})();
|