Add saved analysis workspace and live dashboard statistics
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
const root=document.getElementById('live-analyses');if(!root)return;
|
||||
const grid=document.getElementById('live-analysis-grid'),status=document.getElementById('live-analysis-status');
|
||||
const states=new Map();let refreshing=false;
|
||||
const el=StrideCharts.element;
|
||||
async function api(path,method='GET',payload){const response=await fetch('/stats/analyses'+path,{method,headers:{'Content-Type':'application/json','X-CSRFToken':root.dataset.csrf},...(payload?{body:JSON.stringify(payload)}:{})});const result=response.status===204?{}:await response.json();if(!response.ok)throw new Error(result.error||'Could not update this widget.');return result;}
|
||||
function show(widget,result){
|
||||
const id=widget.dataset.id,old=states.get(id);old?.dispose?.();
|
||||
const body=widget.querySelector('.live-widget-body'),insights=widget.querySelector('.live-widget-insights');body.replaceChildren();insights.replaceChildren();
|
||||
const url=(bucket,scope='group')=>'/stats?'+new URLSearchParams({analysis:id,query:result.analysis.query,...(bucket?{bucket,scope}:{})})+'#studio-evidence';
|
||||
const data=result.results;
|
||||
let dispose=null;
|
||||
if(!data.count)body.append(el('p','field-help','No runs match this analysis yet. Explore to adjust the filters.'));
|
||||
else if(widget.dataset.view==='chart'&&(data.chart.type!=='none'||data.chart.scatter))dispose=StrideCharts.render(body,data.chart,bucket=>location.assign(url(bucket,'trend')),{compact:true});
|
||||
else if(widget.dataset.view==='value'||(widget.dataset.view==='chart'&&data.chart.type==='none')){
|
||||
const values=el('div','live-widget-values');data.cards.forEach(card=>{const item=el('div');item.append(el('span','eyebrow',card.label),el('strong','',`${card.value} ${card.unit}`),el('small','field-help',`${card.coverage} of ${data.count} usable ${data.source}`));values.append(item);});body.append(values);
|
||||
}else{
|
||||
const wrap=el('div','activity-table-wrap'),table=el('table','activity-table'),head=el('thead'),tr=el('tr');['Group',data.source,...data.cards.map(c=>`${c.label} · ${c.unit}`)].forEach(t=>tr.append(el('th','',t)));head.append(tr);table.append(head);const tbody=el('tbody');data.groups.slice(-12).forEach(group=>{const row=el('tr'),cell=el('td'),a=el('a','',group.label);a.href=url(group.label);cell.append(a);row.append(cell);[group.count,...group.values].forEach(v=>row.append(el('td','',v)));tbody.append(row);});table.append(tbody);wrap.append(table);body.append(wrap);if(data.groups.length>12)body.append(el('p','field-help','Last 12 displayed groups · Explore for all results.'));
|
||||
}
|
||||
StrideCharts.insights(insights,data.insights,bucket=>location.assign(url(bucket)));
|
||||
widget.querySelector('.widget-status').textContent=`${data.count} matching ${data.source} · ${data.start} – ${data.end} · updated ${new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}`;
|
||||
states.set(id,{result,dispose});
|
||||
}
|
||||
async function refresh(){
|
||||
if(refreshing||document.hidden)return;refreshing=true;document.getElementById('refresh-analyses').disabled=true;
|
||||
const widgets=[...grid.querySelectorAll('[data-id]')];let index=0;
|
||||
async function worker(){while(index<widgets.length){const widget=widgets[index++];try{const result=await api('/'+widget.dataset.id+'/results');if(widget.isConnected)show(widget,result);}catch(error){if(widget.isConnected)widget.querySelector('.widget-status').textContent=error.message+' Open the analysis in Stats or refresh to retry.';}}}
|
||||
await Promise.all([worker(),worker()]);refreshing=false;document.getElementById('refresh-analyses').disabled=false;
|
||||
}
|
||||
grid.addEventListener('click',async event=>{
|
||||
const button=event.target.closest('button[data-widget-action]');if(!button)return;const widget=button.closest('[data-id]'),id=widget.dataset.id,action=button.dataset.widgetAction;button.disabled=true;
|
||||
try{
|
||||
if(action==='unpin'){await api('/'+id,'PATCH',{pinned:false});states.get(id)?.dispose?.();states.delete(id);widget.remove();status.textContent='Analysis remains in your account library.';}
|
||||
else if(action==='width'){const full=!widget.classList.contains('widget-full');await api('/'+id,'PATCH',{width:full?'full':'half'});widget.classList.toggle('widget-full',full);button.textContent=full?'Half width':'Full width';}
|
||||
else{
|
||||
const widgets=[...grid.querySelectorAll('[data-id]')],index=widgets.indexOf(widget),next=action==='up'?index-1:index+1;if(next<0||next>=widgets.length)return;[widgets[index],widgets[next]]=[widgets[next],widgets[index]];await api('/order','POST',{ids:widgets.map(w=>Number(w.dataset.id))});widgets.forEach(w=>grid.append(w));
|
||||
}
|
||||
}catch(error){status.textContent=error.message;}finally{button.disabled=false;}
|
||||
});
|
||||
grid.addEventListener('change',async event=>{const control=event.target;if(control.dataset.widgetAction!=='view')return;const widget=control.closest('[data-id]'),before=widget.dataset.view;control.disabled=true;try{await api('/'+widget.dataset.id,'PATCH',{view:control.value});widget.dataset.view=control.value;const state=states.get(widget.dataset.id);if(state)show(widget,state.result);}catch(error){control.value=before;status.textContent=error.message;}finally{control.disabled=false;}});
|
||||
document.getElementById('refresh-analyses').onclick=refresh;
|
||||
let lastFocus=0;window.addEventListener('focus',()=>{if(Date.now()-lastFocus>60000){lastFocus=Date.now();refresh();}});
|
||||
const interval=setInterval(refresh,300000);window.addEventListener('pagehide',()=>{clearInterval(interval);states.forEach(state=>state.dispose?.());});
|
||||
refresh();
|
||||
})();
|
||||
Reference in New Issue
Block a user