@adcops/autocore-react 3.3.82 → 3.3.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,6 +65,9 @@ export interface TestDataViewProps {
65
65
  throttleMs?: number;
66
66
  /** Fixed cycle-table scroll height. Default "400px". */
67
67
  cycleTableHeight?: string;
68
+ /** Height of the unified chart panel (any CSS length). Default "320px".
69
+ * Set to e.g. "50vh" for a taller chart on a single-test page. */
70
+ chartHeight?: string;
68
71
  }
69
72
  export declare const TestDataView: React.FC<TestDataViewProps>;
70
73
  //# sourceMappingURL=TestDataView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TestDataView.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestDataView.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAwE,MAAM,OAAO,CAAC;AA6B7F,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IAAI,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAAE;AAChF,MAAM,WAAW,WAAW;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAAE;AAC5G,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IACpC,CAAC,EAAE,SAAS,CAAC;IACb,CAAC,EAAE,WAAW,EAAE,CAAC;CACpB;AACD,MAAM,WAAW,SAAS;IAAG,MAAM,EAAE,MAAM,CAAC;CAAE;AAC9C,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB;;8CAE0C;IAC1C,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACtC,KAAK,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACrC;AACD,MAAM,WAAW,UAAU;IACvB,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,aAAa,EAAI,YAAY,EAAE,CAAC;IAChC,YAAY,EAAK,YAAY,EAAE,CAAC;IAChC,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAQ,YAAY,GAAG,IAAI,CAAC;IACrC,KAAK,CAAC,EAAW;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IAC/C,wDAAwD;IACxD,KAAK,CAAC,EAAW,MAAM,CAAC;IACxB,qDAAqD;IACrD,WAAW,CAAC,EAAK,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAC9B,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,CAAC,EAAG,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAM,MAAM,CAAC;IACnB,kEAAkE;IAClE,MAAM,CAAC,EAAK,UAAU,CAAC;IACvB,8EAA8E;IAC9E,UAAU,CAAC,EAAG,MAAM,CAAC;IACrB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAgcpD,CAAC"}
1
+ {"version":3,"file":"TestDataView.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestDataView.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAwE,MAAM,OAAO,CAAC;AA8B7F,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IAAI,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAAE;AAChF,MAAM,WAAW,WAAW;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAAE;AAC5G,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IACpC,CAAC,EAAE,SAAS,CAAC;IACb,CAAC,EAAE,WAAW,EAAE,CAAC;CACpB;AACD,MAAM,WAAW,SAAS;IAAG,MAAM,EAAE,MAAM,CAAC;CAAE;AAC9C,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB;;8CAE0C;IAC1C,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACtC,KAAK,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACrC;AACD,MAAM,WAAW,UAAU;IACvB,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,aAAa,EAAI,YAAY,EAAE,CAAC;IAChC,YAAY,EAAK,YAAY,EAAE,CAAC;IAChC,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAQ,YAAY,GAAG,IAAI,CAAC;IACrC,KAAK,CAAC,EAAW;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IAC/C,wDAAwD;IACxD,KAAK,CAAC,EAAW,MAAM,CAAC;IACxB,qDAAqD;IACrD,WAAW,CAAC,EAAK,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAC9B,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,CAAC,EAAG,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAM,MAAM,CAAC;IACnB,kEAAkE;IAClE,MAAM,CAAC,EAAK,UAAU,CAAC;IACvB,8EAA8E;IAC9E,UAAU,CAAC,EAAG,MAAM,CAAC;IACrB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;uEACmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAygBpD,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useCallback,useContext,useEffect,useMemo,useRef,useState}from"react";import{Button}from"primereact/button";import{Column}from"primereact/column";import{DataTable}from"primereact/datatable";import{Dialog}from"primereact/dialog";import{Dropdown}from"primereact/dropdown";import{TabView,TabPanel}from"primereact/tabview";import{Chart as ChartJS,CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend}from"chart.js";import zoomPlugin from"chartjs-plugin-zoom";import{Line}from"react-chartjs-2";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";ChartJS.register(CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend,zoomPlugin);export const TestDataView=e=>{const t=useTis(),r=e.projectId??t.selection.projectId,a=e.methodId??t.selection.methodId,s=e.runId??t.selection.runId,l=e.schema??(a?t.schemas[a]:void 0),{throttleMs:i=100,cycleTableHeight:n="400px"}=e,{invoke:o,subscribe:c,unsubscribe:d}=useContext(EventEmitterContext),[m,u]=useState(null),[p,y]=useState([]),[h,f]=useState({}),[g,x]=useState(!1),[_,b]=useState(!1),[j,v]=useState(null),[w,C]=useState(null),[S,T]=useState(!1),[R,N]=useState(null),[D,I]=useState(null),[E,A]=useState(!1),[O,k]=useState([]),[L,M]=useState(null),z=useMemo(()=>{const e=[];for(const[t,r]of Object.entries(l?.views??{}))"cycle_scatter"===r.type&&e.push({name:t,view:r});return e},[l]),[H,P]=useState(z.length>0?z[0].name:null);useEffect(()=>{if(0===z.length)return;null!==H&&z.some(e=>e.name===H)||P(z[0].name)},[z,H]);const F=useRef([]),$=useRef(null),V=useRef(null),B=()=>{V.current||(V.current=setTimeout(()=>{if(V.current=null,F.current.length>0){const e=F.current;F.current=[],y(t=>[...e.slice().reverse(),...t])}$.current&&(f($.current),$.current=null)},i))};useEffect(()=>{if(!r||!a||!s)return u(null),y([]),void f({});let e=!1;return(async()=>{try{const t=await o("tis.read_test",MessageType.Request,{project_id:r,method_id:a,run_id:s});!e&&t?.success&&(u(t.data),f(t.data.results??{}));const l=await o("tis.read_cycles",MessageType.Request,{project_id:r,method_id:a,run_id:s,offset:0,limit:200,order:"desc"});!e&&l?.success&&y(l.data.cycles??[])}catch(e){}})(),()=>{e=!0}},[r,a,s,o]),useEffect(()=>{const e=e=>e?.project_id===r&&e?.method_id===a&&e?.run_id===s,t=c("tis.cycle_added",t=>{e(t)&&t.cycle&&(F.current.push(t.cycle),B())}),l=c("tis.results_updated",t=>{e(t)&&($.current=t.results??{},B())});return()=>{d(t),d(l),V.current&&(clearTimeout(V.current),V.current=null)}},[r,a,s,i]);const q=useMemo(()=>{if(!H||0===z.length)return null;const e=z.find(e=>e.name===H)?.view;if(!e)return null;const t=e.x.field,r=[...p].reverse();return{labels:r.map(e=>e[t]),datasets:e.y.map((e,t)=>({label:e.label??e.field,data:r.map(t=>t[e.field]),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(t),backgroundColor:palette(t),tension:.1,pointRadius:2}))}},[p,H,z]),J=z.find(e=>e.name===H)?.view,G=J?.y.some(e=>"right"===e.y_axis)??!1,W=useMemo(()=>({responsive:!0,maintainAspectRatio:!1,scales:{x:{title:{display:!!J?.x.label,text:J?.x.label}},y:{position:"left",title:{display:!0,text:leftAxisLabel(J)}},...G?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:rightAxisLabel(J)}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},mode:"xy"}}}}),[J,G]),Y=l?.raw_data?.blob_name??"trace",K=useRef(""),Q=useCallback(async()=>{if(!r||!a||!s)return[];try{const e=await o("tis.list_raw",MessageType.Request,{project_id:r,method_id:a,run_id:s});if(!e?.success)return[];const t=e.data?.cycles??[];return t.filter(e=>e?.name===Y&&"number"==typeof e?.cycle_index).map(e=>e.cycle_index).sort((e,t)=>e-t)}catch{return[]}},[r,a,s,Y,o]),U=useCallback(async e=>{if(!r||!a||!s)return;const t=`${r}|${a}|${s}|${Y}|${e??"latest"}`;if(K.current===t)return;K.current=t;const l={project_id:r,method_id:a,run_id:s,name:Y};null!=e&&(l.cycle_index=e),T(!0),C(null),v(null);try{const e=await o("tis.read_raw",MessageType.Request,l);e?.success?v(e.data??{}):C(e?.error_message??"No raw data on disk for this run.")}catch(e){C(String(e?.message??e))}finally{T(!1)}A(!0),I(null),N(null);try{const e=await o("tis.read_filtered",MessageType.Request,{project_id:r,method_id:a,run_id:s,name:Y});e?.success?N(e.data??{}):I(e?.error_message??"No filtered data on disk for this run.")}catch(e){I(String(e?.message??e))}finally{A(!1)}},[r,a,s,Y,o]);useEffect(()=>{K.current="",k([]),M(null)},[r,a,s,Y]);return useEffect(()=>{g&&null!=L&&U(L)},[g,L,U]),r&&a&&s&&l?_jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsx(Header,{meta:m,config:m?.config,runId:s,projectId:r,methodId:a,canViewRaw:!!l.raw_data,onViewRaw:async()=>{x(!0);let e=O;0===e.length&&(e=await Q(),k(e));const t=e.length>0?e[e.length-1]:null,r=L??t;L!==r&&M(r),await U(r)},onShowConfig:()=>b(!0)}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsxs("div",{className:"flex",style:{gap:"1rem",alignItems:"center",marginBottom:"0.5rem"},children:[_jsx(Dropdown,{value:H,options:z.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>P(e.value),placeholder:0===z.length?"No view defined":"Select a view",disabled:0===z.length}),_jsx("h3",{style:{margin:0},children:J?.title??""})]}),_jsx("div",{style:{height:320},children:q&&_jsx(Line,{data:q,options:W})})]}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsxs("h3",{style:{marginTop:0},children:["Cycle Data (",p.length,")"]}),_jsx(DataTable,{value:p,scrollable:!0,scrollHeight:n,virtualScrollerOptions:{itemSize:38},emptyMessage:"No cycles yet.",children:l.cycle_fields.map(e=>_jsx(Column,{field:e.name,header:e.units?`${e.name} (${e.units})`:e.name,body:t=>formatCell(t[e.name],e.type)},e.name))})]}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsx("h3",{style:{marginTop:0},children:"Results"}),_jsx(ResultsGrid,{schema:l.results_fields,values:h})]}),l.raw_data&&_jsxs(Dialog,{visible:g,onHide:()=>x(!1),header:`Run Data — ${s}`,style:{width:"90vw",height:"80vh"},maximizable:!0,children:[_jsx(CyclePickerBar,{cycles:O,selected:L,onChange:M}),_jsx(RawEnvelopeHeader,{envelope:j}),_jsxs(TabView,{style:{height:"100%"},children:[_jsx(TabPanel,{header:"Raw Data",children:_jsx(DataBlobTable,{blob:unwrapEnvelope(j),loading:S,error:w,rawData:l.raw_data})}),_jsx(TabPanel,{header:"Filtered Data",children:_jsx(DataBlobTable,{blob:unwrapEnvelope(R),loading:E,error:D,rawData:l.raw_data,emptyMessage:"Filtered data is written by post-processing — none on disk for this run yet."})})]})]}),_jsx(Dialog,{visible:_,onHide:()=>b(!1),header:"Test Configuration",style:{width:"min(640px, 90vw)"},modal:!0,children:_jsx(ConfigList,{config:m?.config})})]}):_jsx("div",{className:"p-card",style:{padding:"1rem",color:"var(--text-secondary-color)"},children:"No test selected. Pick a row from the History tab or start a run."})};const Header=({meta:e,config:t,runId:r,projectId:a,methodId:s,canViewRaw:l,onViewRaw:i,onShowConfig:n})=>{const o="string"==typeof e?.sample_id&&e.sample_id||"string"==typeof e?.config?.sample_id&&e.config.sample_id||"",c=t&&"object"==typeof t&&Object.entries(t).some(([e])=>"sample_id"!==e);return _jsx("div",{className:"p-card",style:{padding:"1rem"},children:_jsxs("div",{className:"flex",style:{justifyContent:"space-between",alignItems:"flex-start",gap:"1rem"},children:[_jsxs("div",{children:[_jsxs("h2",{style:{margin:0,display:"flex",alignItems:"center",gap:"0.5rem"},children:[o||r,c&&_jsx(Button,{icon:"pi pi-info-circle",type:"button",rounded:!0,text:!0,onClick:n,tooltip:"Show test configuration",tooltipOptions:{position:"top"},style:{width:"2rem",height:"2rem",padding:0},"aria-label":"Show test configuration"})]}),_jsxs("div",{style:{color:"var(--text-secondary-color)",fontSize:"0.85em"},children:["project: ",a," · method: ",s," · run: ",r,e?.start_time&&_jsxs(_Fragment,{children:[" · started: ",new Date(e.start_time).toLocaleString()]})]})]}),l&&_jsx(Button,{icon:"pi pi-table",label:"View Raw Data",onClick:i,outlined:!0})]})})},ConfigList=({config:e})=>{const t=e&&"object"==typeof e?Object.entries(e).filter(([e])=>"sample_id"!==e):[];return 0===t.length?_jsx("div",{style:{color:"var(--text-secondary-color)"},children:"No configuration recorded for this run."}):_jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",fontSize:"0.95em"},children:t.map(([e,t])=>_jsxs(React.Fragment,{children:[_jsx("div",{style:{color:"var(--text-secondary-color)"},children:e}),_jsx("div",{children:formatCell(t,"string")})]},e))})},unwrapEnvelope=e=>e&&"object"==typeof e&&"data"in e&&e.data&&"object"==typeof e.data&&Object.values(e.data).some(e=>Array.isArray(e))?e.data:e,CyclePickerBar=({cycles:e,selected:t,onChange:r})=>e.length<=1?null:_jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem",padding:"0.25rem 0.5rem 0.5rem"},children:[_jsx("label",{htmlFor:"raw-cycle-picker",style:{color:"var(--text-secondary-color)"},children:"Cycle:"}),_jsx(Dropdown,{inputId:"raw-cycle-picker",value:t,options:e.map(e=>({label:`Cycle ${e}`,value:e})),onChange:e=>r(Number(e.value)),style:{minWidth:"8rem"}}),_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:["(",e.length," cycles recorded)"]})]}),RawEnvelopeHeader=({envelope:e})=>{if(!e||"object"!=typeof e)return null;const t=e.cycle_index,r=e.cycle_fields,a=e.context;if(null==t&&!r&&!a)return null;const s=e=>{if(!e||"object"!=typeof e)return null;const t=Object.entries(e).filter(([,e])=>null!==e&&"object"!=typeof e);return 0===t.length?null:t.map(([e,t])=>_jsxs("span",{style:{marginRight:"1rem"},children:[_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:[e,": "]}),_jsx("span",{children:String(t)})]},e))};return _jsxs("div",{style:{padding:"0.5rem",borderBottom:"1px solid var(--surface-border)",fontSize:"0.9rem"},children:[null!=t&&_jsx("div",{children:_jsxs("strong",{children:["Cycle ",t]})}),r&&_jsx("div",{style:{marginTop:"0.25rem"},children:s(r)}),a&&_jsx("div",{style:{marginTop:"0.25rem"},children:s(a)})]})},DataBlobTable=({blob:e,loading:t,error:r,rawData:a,emptyMessage:s})=>{if(t)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:"Loading…"});if(r)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:s??r});if(!e||"object"!=typeof e)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:"No data."});const l=Object.keys(a.columns??{}),i=Object.keys(e).filter(t=>Array.isArray(e[t])),n=[];for(const t of l)Array.isArray(e[t])&&n.push(t);for(const e of i)n.includes(e)||n.push(e);if(0===n.length)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:s??"No columnar data in this blob."});const o=n.reduce((t,r)=>Math.min(t,e[r].length),Number.POSITIVE_INFINITY),c=Number.isFinite(o)?o:0,d=Array.from({length:c},(t,r)=>{const a={__i:r};for(const t of n)a[t]=e[t][r];return a}),m=e=>{const t=a.units?.[e];return t?`${e} [${t}]`:e};return _jsxs(DataTable,{value:d,scrollable:!0,scrollHeight:"60vh",virtualScrollerOptions:{itemSize:32},emptyMessage:s??"No data.",size:"small",stripedRows:!0,children:[_jsx(Column,{field:"__i",header:"#",style:{width:"5rem",textAlign:"right"},bodyStyle:{fontVariantNumeric:"tabular-nums",textAlign:"right"}}),n.map(e=>_jsx(Column,{field:e,header:m(e),style:{minWidth:"8rem"},bodyStyle:{fontVariantNumeric:"tabular-nums",textAlign:"right"},body:t=>formatNumeric(t[e])},e))]})},formatNumeric=e=>null==e?"":"number"==typeof e&&Number.isFinite(e)?Number.parseFloat(e.toPrecision(6)).toString():String(e),ResultsGrid=({schema:e,values:t})=>t&&0!==Object.keys(t).length?_jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(220px, 1fr))",gap:"0.5rem 1rem"},children:e.map(e=>_jsxs("div",{children:[_jsxs("div",{style:{fontSize:"0.8em",color:"var(--text-secondary-color)"},children:[e.name,e.units?` (${e.units})`:""]}),_jsx("div",{children:formatCell(t[e.name],e.type)})]},e.name))}):_jsx("div",{style:{color:"var(--text-secondary-color)"},children:"No results yet."}),CHART_COLORS=["#4ea8de","#f59e0b","#22c55e","#a855f7","#ef4444","#14b8a6","#eab308","#ec4899"],palette=e=>CHART_COLORS[e%CHART_COLORS.length],leftAxisLabel=e=>e?.y.filter(e=>"right"!==e.y_axis).map(e=>e.label??e.field).join(" / ")??"",rightAxisLabel=e=>e?.y.filter(e=>"right"===e.y_axis).map(e=>e.label??e.field).join(" / ")??"",formatCell=(e,t)=>null==e?"":"f32"===t||"f64"===t?"number"==typeof e?e.toFixed(4):String(e):"object"==typeof e?JSON.stringify(e):String(e);
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useCallback,useContext,useEffect,useMemo,useRef,useState}from"react";import{Button}from"primereact/button";import{Column}from"primereact/column";import{DataTable}from"primereact/datatable";import{Dialog}from"primereact/dialog";import{Dropdown}from"primereact/dropdown";import{TabView,TabPanel}from"primereact/tabview";import{Chart as ChartJS,CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend}from"chart.js";import zoomPlugin from"chartjs-plugin-zoom";import{Line}from"react-chartjs-2";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";import{useRawCycleData}from"./useRawCycleData";ChartJS.register(CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend,zoomPlugin);export const TestDataView=e=>{const t=useTis(),r=e.projectId??t.selection.projectId,a=e.methodId??t.selection.methodId,l=e.runId??t.selection.runId,s=e.schema??(a?t.schemas[a]:void 0),{throttleMs:n=100,cycleTableHeight:i="400px",chartHeight:o="320px"}=e,{invoke:c,subscribe:d,unsubscribe:u}=useContext(EventEmitterContext),[m,p]=useState(null),[y,h]=useState([]),[f,g]=useState({}),[x,_]=useState(!1),[b,j]=useState(!1),[v,w]=useState(null),[C,S]=useState(null),[T,R]=useState(!1),[N,D]=useState(null),[I,L]=useState(null),[E,O]=useState(!1),[k,A]=useState([]),[M,F]=useState(null),H=useMemo(()=>{const e=[];for(const[t,r]of Object.entries(s?.views??{}))e.push({name:t,view:r});return e},[s]),[z,P]=useState(H.length>0?H[0].name:null);useEffect(()=>{if(0===H.length)return;null!==z&&H.some(e=>e.name===z)||P(H[0].name)},[H,z]);const $=H.find(e=>e.name===z)?.view,V="raw_trace"===$?.type,B=useRef([]),q=useRef(null),W=useRef(null),J=()=>{W.current||(W.current=setTimeout(()=>{if(W.current=null,B.current.length>0){const e=B.current;B.current=[],h(t=>[...e.slice().reverse(),...t])}q.current&&(g(q.current),q.current=null)},n))};useEffect(()=>{if(!r||!a||!l)return p(null),h([]),void g({});let e=!1;return(async()=>{try{const t=await c("tis.read_test",MessageType.Request,{project_id:r,method_id:a,run_id:l});!e&&t?.success&&(p(t.data),g(t.data.results??{}));const s=await c("tis.read_cycles",MessageType.Request,{project_id:r,method_id:a,run_id:l,offset:0,limit:200,order:"desc"});!e&&s?.success&&h(s.data.cycles??[])}catch(e){}})(),()=>{e=!0}},[r,a,l,c]),useEffect(()=>{const e=e=>e?.project_id===r&&e?.method_id===a&&e?.run_id===l,t=d("tis.cycle_added",t=>{e(t)&&t.cycle&&(B.current.push(t.cycle),J())}),s=d("tis.results_updated",t=>{e(t)&&(q.current=t.results??{},J())});return()=>{u(t),u(s),W.current&&(clearTimeout(W.current),W.current=null)}},[r,a,l,n]);const G=useRawCycleData({projectId:r,methodId:a,runId:l,blobName:s?.raw_data?.blob_name??"trace",enabled:V}),Y=useMemo(()=>{if(!$)return null;if("cycle_scatter"===$.type){const e=$.x.field;if(!e)return null;const t=[...y].reverse();return{labels:t.map(t=>t[e]),datasets:$.y.map((e,r)=>({label:e.label??e.field,data:t.map(t=>t[e.field]),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(r),backgroundColor:palette(r),tension:.1,pointRadius:2}))}}if("raw_trace"===$.type){if(!G.raw)return null;const e=$.x.column;if(!e)return null;const t=G.raw[e]??[];return{datasets:$.y.map((e,r)=>({label:e.label??e.column,data:(G.raw[e.column]??[]).map((e,r)=>({x:t[r],y:e})),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(r),backgroundColor:palette(r),pointRadius:0,borderWidth:1.5,showLine:!0}))}}return null},[$,y,G.raw]),K=$?.y.some(e=>"right"===e.y_axis)??!1,Q=useMemo(()=>{const e="raw_trace"===$?.type;return{responsive:!0,maintainAspectRatio:!1,parsing:!e&&void 0,scales:{x:e?{type:"linear",title:{display:!!$?.x.label,text:$?.x.label}}:{title:{display:!!$?.x.label,text:$?.x.label}},y:{position:"left",title:{display:!0,text:leftAxisLabel($)}},...K?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:rightAxisLabel($)}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},mode:"xy"}}}}},[$,K]),U=s?.raw_data?.blob_name??"trace",X=useRef(""),Z=useCallback(async()=>{if(!r||!a||!l)return[];try{const e=await c("tis.list_raw",MessageType.Request,{project_id:r,method_id:a,run_id:l});if(!e?.success)return[];const t=e.data?.cycles??[];return t.filter(e=>e?.name===U&&"number"==typeof e?.cycle_index).map(e=>e.cycle_index).sort((e,t)=>e-t)}catch{return[]}},[r,a,l,U,c]),ee=useCallback(async e=>{if(!r||!a||!l)return;const t=`${r}|${a}|${l}|${U}|${e??"latest"}`;if(X.current===t)return;X.current=t;const s={project_id:r,method_id:a,run_id:l,name:U};null!=e&&(s.cycle_index=e),R(!0),S(null),w(null);try{const e=await c("tis.read_raw",MessageType.Request,s);e?.success?w(e.data??{}):S(e?.error_message??"No raw data on disk for this run.")}catch(e){S(String(e?.message??e))}finally{R(!1)}O(!0),L(null),D(null);try{const e=await c("tis.read_filtered",MessageType.Request,{project_id:r,method_id:a,run_id:l,name:U});e?.success?D(e.data??{}):L(e?.error_message??"No filtered data on disk for this run.")}catch(e){L(String(e?.message??e))}finally{O(!1)}},[r,a,l,U,c]);useEffect(()=>{X.current="",A([]),F(null)},[r,a,l,U]);return useEffect(()=>{x&&null!=M&&ee(M)},[x,M,ee]),r&&a&&l&&s?_jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsx(Header,{meta:m,config:m?.config,runId:l,projectId:r,methodId:a,canViewRaw:!!s.raw_data,onViewRaw:async()=>{_(!0);let e=k;0===e.length&&(e=await Z(),A(e));const t=e.length>0?e[e.length-1]:null,r=M??t;M!==r&&F(r),await ee(r)},onShowConfig:()=>j(!0)}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsxs("div",{className:"flex",style:{gap:"1rem",alignItems:"center",marginBottom:"0.5rem",flexWrap:"wrap"},children:[_jsx(Dropdown,{value:z,options:H.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>P(e.value),placeholder:0===H.length?"No view defined":"Select a view",disabled:0===H.length}),_jsx("h3",{style:{margin:0},children:$?.title??""}),V&&G.cycles.length>1&&_jsxs(_Fragment,{children:[_jsx("label",{htmlFor:"chart-cycle-picker",style:{color:"var(--text-secondary-color)"},children:"Cycle:"}),_jsx(Dropdown,{inputId:"chart-cycle-picker",value:G.selectedCycle,options:G.cycles.map(e=>({label:`Cycle ${e}`,value:e})),onChange:e=>G.setSelectedCycle(Number(e.value)),style:{minWidth:"8rem"}}),_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:["of ",G.cycles.length]})]})]}),_jsxs("div",{style:{height:o,position:"relative"},children:[V&&G.loading&&_jsx(ChartOverlay,{children:"Loading raw data…"}),V&&G.error&&_jsx(ChartOverlay,{children:G.error}),Y&&_jsx(Line,{data:Y,options:Q})]})]}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsxs("h3",{style:{marginTop:0},children:["Cycle Data (",y.length,")"]}),_jsx(DataTable,{value:y,scrollable:!0,scrollHeight:i,virtualScrollerOptions:{itemSize:38},emptyMessage:"No cycles yet.",children:s.cycle_fields.map(e=>_jsx(Column,{field:e.name,header:e.units?`${e.name} (${e.units})`:e.name,body:t=>formatCell(t[e.name],e.type)},e.name))})]}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsx("h3",{style:{marginTop:0},children:"Results"}),_jsx(ResultsGrid,{schema:s.results_fields,values:f})]}),s.raw_data&&_jsxs(Dialog,{visible:x,onHide:()=>_(!1),header:`Run Data — ${l}`,style:{width:"90vw",height:"80vh"},maximizable:!0,children:[_jsx(CyclePickerBar,{cycles:k,selected:M,onChange:F}),_jsx(RawEnvelopeHeader,{envelope:v}),_jsxs(TabView,{style:{height:"100%"},children:[_jsx(TabPanel,{header:"Raw Data",children:_jsx(DataBlobTable,{blob:unwrapEnvelope(v),loading:T,error:C,rawData:s.raw_data})}),_jsx(TabPanel,{header:"Filtered Data",children:_jsx(DataBlobTable,{blob:unwrapEnvelope(N),loading:E,error:I,rawData:s.raw_data,emptyMessage:"Filtered data is written by post-processing — none on disk for this run yet."})})]})]}),_jsx(Dialog,{visible:b,onHide:()=>j(!1),header:"Test Configuration",style:{width:"min(640px, 90vw)"},modal:!0,children:_jsx(ConfigList,{config:m?.config})})]}):_jsx("div",{className:"p-card",style:{padding:"1rem",color:"var(--text-secondary-color)"},children:"No test selected. Pick a row from the History tab or start a run."})};const Header=({meta:e,config:t,runId:r,projectId:a,methodId:l,canViewRaw:s,onViewRaw:n,onShowConfig:i})=>{const o="string"==typeof e?.sample_id&&e.sample_id||"string"==typeof e?.config?.sample_id&&e.config.sample_id||"",c=t&&"object"==typeof t&&Object.entries(t).some(([e])=>"sample_id"!==e);return _jsx("div",{className:"p-card",style:{padding:"1rem"},children:_jsxs("div",{className:"flex",style:{justifyContent:"space-between",alignItems:"flex-start",gap:"1rem"},children:[_jsxs("div",{children:[_jsxs("h2",{style:{margin:0,display:"flex",alignItems:"center",gap:"0.5rem"},children:[o||r,c&&_jsx(Button,{icon:"pi pi-info-circle",type:"button",rounded:!0,text:!0,onClick:i,tooltip:"Show test configuration",tooltipOptions:{position:"top"},style:{width:"2rem",height:"2rem",padding:0},"aria-label":"Show test configuration"})]}),_jsxs("div",{style:{color:"var(--text-secondary-color)",fontSize:"0.85em"},children:["project: ",a," · method: ",l," · run: ",r,e?.start_time&&_jsxs(_Fragment,{children:[" · started: ",new Date(e.start_time).toLocaleString()]})]})]}),s&&_jsx(Button,{icon:"pi pi-table",label:"View Raw Data",onClick:n,outlined:!0})]})})},ConfigList=({config:e})=>{const t=e&&"object"==typeof e?Object.entries(e).filter(([e])=>"sample_id"!==e):[];return 0===t.length?_jsx("div",{style:{color:"var(--text-secondary-color)"},children:"No configuration recorded for this run."}):_jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",fontSize:"0.95em"},children:t.map(([e,t])=>_jsxs(React.Fragment,{children:[_jsx("div",{style:{color:"var(--text-secondary-color)"},children:e}),_jsx("div",{children:formatCell(t,"string")})]},e))})},unwrapEnvelope=e=>e&&"object"==typeof e&&"data"in e&&e.data&&"object"==typeof e.data&&Object.values(e.data).some(e=>Array.isArray(e))?e.data:e,CyclePickerBar=({cycles:e,selected:t,onChange:r})=>e.length<=1?null:_jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem",padding:"0.25rem 0.5rem 0.5rem"},children:[_jsx("label",{htmlFor:"raw-cycle-picker",style:{color:"var(--text-secondary-color)"},children:"Cycle:"}),_jsx(Dropdown,{inputId:"raw-cycle-picker",value:t,options:e.map(e=>({label:`Cycle ${e}`,value:e})),onChange:e=>r(Number(e.value)),style:{minWidth:"8rem"}}),_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:["(",e.length," cycles recorded)"]})]}),RawEnvelopeHeader=({envelope:e})=>{if(!e||"object"!=typeof e)return null;const t=e.cycle_index,r=e.cycle_fields,a=e.context;if(null==t&&!r&&!a)return null;const l=e=>{if(!e||"object"!=typeof e)return null;const t=Object.entries(e).filter(([,e])=>null!==e&&"object"!=typeof e);return 0===t.length?null:t.map(([e,t])=>_jsxs("span",{style:{marginRight:"1rem"},children:[_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:[e,": "]}),_jsx("span",{children:String(t)})]},e))};return _jsxs("div",{style:{padding:"0.5rem",borderBottom:"1px solid var(--surface-border)",fontSize:"0.9rem"},children:[null!=t&&_jsx("div",{children:_jsxs("strong",{children:["Cycle ",t]})}),r&&_jsx("div",{style:{marginTop:"0.25rem"},children:l(r)}),a&&_jsx("div",{style:{marginTop:"0.25rem"},children:l(a)})]})},DataBlobTable=({blob:e,loading:t,error:r,rawData:a,emptyMessage:l})=>{if(t)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:"Loading…"});if(r)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:l??r});if(!e||"object"!=typeof e)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:"No data."});const s=Object.keys(a.columns??{}),n=Object.keys(e).filter(t=>Array.isArray(e[t])),i=[];for(const t of s)Array.isArray(e[t])&&i.push(t);for(const e of n)i.includes(e)||i.push(e);if(0===i.length)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:l??"No columnar data in this blob."});const o=i.reduce((t,r)=>Math.min(t,e[r].length),Number.POSITIVE_INFINITY),c=Number.isFinite(o)?o:0,d=Array.from({length:c},(t,r)=>{const a={__i:r};for(const t of i)a[t]=e[t][r];return a}),u=e=>{const t=a.units?.[e];return t?`${e} [${t}]`:e};return _jsxs(DataTable,{value:d,scrollable:!0,scrollHeight:"60vh",virtualScrollerOptions:{itemSize:32},emptyMessage:l??"No data.",size:"small",stripedRows:!0,children:[_jsx(Column,{field:"__i",header:"#",style:{width:"5rem",textAlign:"right"},bodyStyle:{fontVariantNumeric:"tabular-nums",textAlign:"right"}}),i.map(e=>_jsx(Column,{field:e,header:u(e),style:{minWidth:"8rem"},bodyStyle:{fontVariantNumeric:"tabular-nums",textAlign:"right"},body:t=>formatNumeric(t[e])},e))]})},formatNumeric=e=>null==e?"":"number"==typeof e&&Number.isFinite(e)?Number.parseFloat(e.toPrecision(6)).toString():String(e),ResultsGrid=({schema:e,values:t})=>t&&0!==Object.keys(t).length?_jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(220px, 1fr))",gap:"0.5rem 1rem"},children:e.map(e=>_jsxs("div",{children:[_jsxs("div",{style:{fontSize:"0.8em",color:"var(--text-secondary-color)"},children:[e.name,e.units?` (${e.units})`:""]}),_jsx("div",{children:formatCell(t[e.name],e.type)})]},e.name))}):_jsx("div",{style:{color:"var(--text-secondary-color)"},children:"No results yet."}),CHART_COLORS=["#4ea8de","#f59e0b","#22c55e","#a855f7","#ef4444","#14b8a6","#eab308","#ec4899"],palette=e=>CHART_COLORS[e%CHART_COLORS.length],ChartOverlay=({children:e})=>_jsx("div",{style:{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",color:"var(--text-secondary-color)",pointerEvents:"none"},children:e}),seriesLabel=e=>e.label??e.field??e.column??"",leftAxisLabel=e=>e?.y.filter(e=>"right"!==e.y_axis).map(seriesLabel).join(" / ")??"",rightAxisLabel=e=>e?.y.filter(e=>"right"===e.y_axis).map(seriesLabel).join(" / ")??"",formatCell=(e,t)=>null==e?"":"f32"===t||"f64"===t?"number"==typeof e?e.toFixed(4):String(e):"object"==typeof e?JSON.stringify(e):String(e);
@@ -1 +1 @@
1
- {"version":3,"file":"TestRawDataView.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestRawDataView.tsx"],"names":[],"mappings":"AAYA,OAAO,KAA2D,MAAM,OAAO,CAAC;AAahF,OAAO,KAAK,EAAa,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAQ5D,MAAM,WAAW,oBAAoB;IACjC,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,CAAC,EAAG,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAM,MAAM,CAAC;IACnB,kEAAkE;IAClE,MAAM,CAAC,EAAK,UAAU,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAuN1D,CAAC"}
1
+ {"version":3,"file":"TestRawDataView.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestRawDataView.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAoC,MAAM,OAAO,CAAC;AAWzD,OAAO,KAAK,EAAa,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAS5D,MAAM,WAAW,oBAAoB;IACjC,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,CAAC,EAAG,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAM,MAAM,CAAC;IACnB,kEAAkE;IAClE,MAAM,CAAC,EAAK,UAAU,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAoJ1D,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useContext,useEffect,useMemo,useRef,useState}from"react";import{Button}from"primereact/button";import{Dropdown}from"primereact/dropdown";import{Chart as ChartJS,CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend}from"chart.js";import zoomPlugin from"chartjs-plugin-zoom";import{Line}from"react-chartjs-2";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";ChartJS.register(CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend,zoomPlugin);export const TestRawDataView=e=>{const t=useTis(),r=e.projectId??t.selection.projectId,a=e.methodId??t.selection.methodId,l=e.runId??t.selection.runId,n=e.schema??(a?t.schemas[a]:void 0),{blobName:s,chartHeight:o="60vh"}=e,{invoke:i}=useContext(EventEmitterContext),[c,d]=useState(null),[m,p]=useState(null),[u,y]=useState(!0),[x,h]=useState(null),f=useRef(null),[g,_]=useState([]),[v,j]=useState(null),b=useMemo(()=>{const e=[];for(const[t,r]of Object.entries(n?.views??{}))"raw_trace"===r.type&&e.push({name:t,view:r});return e},[n]),[w,S]=useState(b.length>0?b[0].name:null),C=s??n?.raw_data?.blob_name??"trace";useEffect(()=>{_([]),j(null)},[r,a,l,C]),useEffect(()=>{if(!r||!a||!l)return;let e=!1;return(async()=>{try{const t=await i("tis.list_raw",MessageType.Request,{project_id:r,method_id:a,run_id:l});if(e||!t?.success)return;const n=(t.data?.cycles??[]).filter(e=>e?.name===C&&"number"==typeof e?.cycle_index).map(e=>e.cycle_index).sort((e,t)=>e-t);_(n),n.length>0&&j(e=>e??n[n.length-1])}catch{}})(),()=>{e=!0}},[r,a,l,C,i]),useEffect(()=>{if(!r||!a||!l)return d(null),p(null),y(!1),void h(null);let e=!1;return y(!0),h(null),(async()=>{try{const t={project_id:r,method_id:a,run_id:l,name:C};null!=v&&(t.cycle_index=v);const n=await i("tis.read_raw",MessageType.Request,t);if(e)return;if(n?.success){const e=n.data??{};p(e),d(unwrapEnvelope(e))}else h(n?.error_message??"Failed to read raw data")}catch(t){e||h(String(t?.message??t))}finally{e||y(!1)}})(),()=>{e=!0}},[r,a,l,C,v,i]);const E=useMemo(()=>{if(!c||!w)return null;const e=b.find(e=>e.name===w)?.view;if(!e)return null;const t=e.x.column,r=c[t]??[];return{datasets:e.y.map((e,t)=>({label:e.label??e.column,data:(c[e.column]??[]).map((e,t)=>({x:r[t],y:e})),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(t),backgroundColor:palette(t),pointRadius:0,borderWidth:1.5,showLine:!0}))}},[c,w,b]),T=b.find(e=>e.name===w)?.view,L=T?.y.some(e=>"right"===e.y_axis)??!1,R=useMemo(()=>({responsive:!0,maintainAspectRatio:!1,parsing:!1,scales:{x:{type:"linear",title:{display:!!T?.x.label,text:T?.x.label}},y:{position:"left",title:{display:!0,text:axisLabel(T,"left")}},...L?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:axisLabel(T,"right")}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},drag:{enabled:!0,modifierKey:"shift"},mode:"xy"}}}}),[T,L]);return r&&a&&l?n?n.raw_data?0===b.length?_jsx(EmptyState,{message:"No raw_trace views declared. Add one to schema.views in project.json."}):_jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem",height:"100%"},children:[_jsxs("div",{className:"flex",style:{gap:"1rem",alignItems:"center",flexWrap:"wrap"},children:[_jsx(Dropdown,{value:w,options:b.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>S(e.value),placeholder:"Select a view"}),_jsx("h3",{style:{margin:0},children:T?.title??""}),g.length>1&&_jsxs(_Fragment,{children:[_jsx("label",{htmlFor:"rawview-cycle-picker",style:{color:"var(--text-secondary-color)"},children:"Cycle:"}),_jsx(Dropdown,{inputId:"rawview-cycle-picker",value:v,options:g.map(e=>({label:`Cycle ${e}`,value:e})),onChange:e=>j(Number(e.value)),style:{minWidth:"8rem"}}),_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:["of ",g.length]})]}),_jsx("div",{style:{flex:1}}),_jsx(Button,{icon:"pi pi-refresh",label:"Reset Zoom",outlined:!0,onClick:()=>f.current?.resetZoom?.()})]}),_jsx(EnvelopeMetaStrip,{envelope:m}),_jsxs("div",{style:{flex:1,minHeight:0,height:o,position:"relative"},children:[u&&_jsx(Overlay,{children:"Loading raw data…"}),x&&_jsx(Overlay,{children:x}),E&&!u&&!x&&_jsx(Line,{ref:f,data:E,options:R})]})]}):_jsx(EmptyState,{message:"No raw_data is declared for this test method."}):_jsx(EmptyState,{message:"Schema not loaded yet."}):_jsx(EmptyState,{message:"No test selected."})};const Overlay=({children:e})=>_jsx("div",{style:{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",color:"var(--text-secondary-color)",pointerEvents:"none"},children:e}),EmptyState=({message:e})=>_jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:e}),unwrapEnvelope=e=>e&&"object"==typeof e?"data"in e&&e.data&&"object"==typeof e.data&&Object.values(e.data).some(e=>Array.isArray(e))?e.data:e:{},EnvelopeMetaStrip=({envelope:e})=>{if(!e||"object"!=typeof e)return null;const t=e.cycle_index,r=e.cycle_fields,a=e.context;if(null==t&&!r&&!a)return null;const l=e=>e&&"object"==typeof e?Object.entries(e).filter(([,e])=>null!==e&&"object"!=typeof e).map(([e,t])=>_jsxs("span",{style:{marginRight:"1rem"},children:[_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:[e,": "]}),_jsx("span",{children:String(t)})]},e)):null;return _jsxs("div",{style:{padding:"0.5rem 0.25rem",fontSize:"0.9rem",borderTop:"1px solid var(--surface-border)",borderBottom:"1px solid var(--surface-border)"},children:[null!=t&&_jsx("div",{children:_jsxs("strong",{children:["Cycle ",t]})}),r&&_jsx("div",{style:{marginTop:"0.25rem"},children:l(r)}),a&&_jsx("div",{style:{marginTop:"0.25rem"},children:l(a)})]})},CHART_COLORS=["#4ea8de","#f59e0b","#22c55e","#a855f7","#ef4444","#14b8a6","#eab308","#ec4899"],palette=e=>CHART_COLORS[e%CHART_COLORS.length],axisLabel=(e,t)=>e?.y.filter(e=>(e.y_axis??"left")===t).map(e=>e.label??e.column).join(" / ")??"";
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useMemo,useRef,useState}from"react";import{Button}from"primereact/button";import{Dropdown}from"primereact/dropdown";import{Chart as ChartJS,CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend}from"chart.js";import zoomPlugin from"chartjs-plugin-zoom";import{Line}from"react-chartjs-2";import{useTis}from"./TisProvider";import{useRawCycleData}from"./useRawCycleData";ChartJS.register(CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend,zoomPlugin);export const TestRawDataView=e=>{const t=useTis(),r=e.projectId??t.selection.projectId,l=e.methodId??t.selection.methodId,a=e.runId??t.selection.runId,o=e.schema??(l?t.schemas[l]:void 0),{blobName:s,chartHeight:n="60vh"}=e,i=useRef(null),c=useMemo(()=>{const e=[];for(const[t,r]of Object.entries(o?.views??{}))"raw_trace"===r.type&&e.push({name:t,view:r});return e},[o]),[d,m]=useState(c.length>0?c[0].name:null),p=s??o?.raw_data?.blob_name??"trace",{cycles:y,selectedCycle:x,setSelectedCycle:h,raw:u,envelope:g,loading:j,error:f}=useRawCycleData({projectId:r,methodId:l,runId:a,blobName:p,enabled:!!r&&!!l&&!!a}),v=useMemo(()=>{if(!u||!d)return null;const e=c.find(e=>e.name===d)?.view;if(!e)return null;const t=e.x.column,r=u[t]??[];return{datasets:e.y.map((e,t)=>({label:e.label??e.column,data:(u[e.column]??[]).map((e,t)=>({x:r[t],y:e})),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(t),backgroundColor:palette(t),pointRadius:0,borderWidth:1.5,showLine:!0}))}},[u,d,c]),_=c.find(e=>e.name===d)?.view,b=_?.y.some(e=>"right"===e.y_axis)??!1,w=useMemo(()=>({responsive:!0,maintainAspectRatio:!1,parsing:!1,scales:{x:{type:"linear",title:{display:!!_?.x.label,text:_?.x.label}},y:{position:"left",title:{display:!0,text:axisLabel(_,"left")}},...b?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:axisLabel(_,"right")}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},drag:{enabled:!0,modifierKey:"shift"},mode:"xy"}}}}),[_,b]);return r&&l&&a?o?o.raw_data?0===c.length?_jsx(EmptyState,{message:"No raw_trace views declared. Add one to schema.views in project.json."}):_jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem",height:"100%"},children:[_jsxs("div",{className:"flex",style:{gap:"1rem",alignItems:"center",flexWrap:"wrap"},children:[_jsx(Dropdown,{value:d,options:c.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>m(e.value),placeholder:"Select a view"}),_jsx("h3",{style:{margin:0},children:_?.title??""}),y.length>1&&_jsxs(_Fragment,{children:[_jsx("label",{htmlFor:"rawview-cycle-picker",style:{color:"var(--text-secondary-color)"},children:"Cycle:"}),_jsx(Dropdown,{inputId:"rawview-cycle-picker",value:x,options:y.map(e=>({label:`Cycle ${e}`,value:e})),onChange:e=>h(Number(e.value)),style:{minWidth:"8rem"}}),_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:["of ",y.length]})]}),_jsx("div",{style:{flex:1}}),_jsx(Button,{icon:"pi pi-refresh",label:"Reset Zoom",outlined:!0,onClick:()=>i.current?.resetZoom?.()})]}),_jsx(EnvelopeMetaStrip,{envelope:g}),_jsxs("div",{style:{flex:1,minHeight:0,height:n,position:"relative"},children:[j&&_jsx(Overlay,{children:"Loading raw data…"}),f&&_jsx(Overlay,{children:f}),v&&!j&&!f&&_jsx(Line,{ref:i,data:v,options:w})]})]}):_jsx(EmptyState,{message:"No raw_data is declared for this test method."}):_jsx(EmptyState,{message:"Schema not loaded yet."}):_jsx(EmptyState,{message:"No test selected."})};const Overlay=({children:e})=>_jsx("div",{style:{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",color:"var(--text-secondary-color)",pointerEvents:"none"},children:e}),EmptyState=({message:e})=>_jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:e}),EnvelopeMetaStrip=({envelope:e})=>{if(!e||"object"!=typeof e)return null;const t=e.cycle_index,r=e.cycle_fields,l=e.context;if(null==t&&!r&&!l)return null;const a=e=>e&&"object"==typeof e?Object.entries(e).filter(([,e])=>null!==e&&"object"!=typeof e).map(([e,t])=>_jsxs("span",{style:{marginRight:"1rem"},children:[_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:[e,": "]}),_jsx("span",{children:String(t)})]},e)):null;return _jsxs("div",{style:{padding:"0.5rem 0.25rem",fontSize:"0.9rem",borderTop:"1px solid var(--surface-border)",borderBottom:"1px solid var(--surface-border)"},children:[null!=t&&_jsx("div",{children:_jsxs("strong",{children:["Cycle ",t]})}),r&&_jsx("div",{style:{marginTop:"0.25rem"},children:a(r)}),l&&_jsx("div",{style:{marginTop:"0.25rem"},children:a(l)})]})},CHART_COLORS=["#4ea8de","#f59e0b","#22c55e","#a855f7","#ef4444","#14b8a6","#eab308","#ec4899"],palette=e=>CHART_COLORS[e%CHART_COLORS.length],axisLabel=(e,t)=>e?.y.filter(e=>(e.y_axis??"left")===t).map(e=>e.label??e.column).join(" / ")??"";
@@ -0,0 +1,39 @@
1
+ export interface UseRawCycleDataOptions {
2
+ projectId?: string;
3
+ methodId?: string;
4
+ runId?: string;
5
+ /** Blob name (e.g. "trace"); usually schema.raw_data.blob_name. */
6
+ blobName: string;
7
+ /** When false, the hook does no fetching and returns empty state.
8
+ * Wire to "is the current chart view a raw_trace?" so scatter
9
+ * selections don't trigger a blob round-trip. */
10
+ enabled: boolean;
11
+ }
12
+ export interface UseRawCycleDataResult {
13
+ /** Per-cycle indices discovered on disk for this run/blob, sorted
14
+ * ascending. Empty when no raw_data has been written yet. */
15
+ cycles: number[];
16
+ selectedCycle: number | null;
17
+ setSelectedCycle: (n: number | null) => void;
18
+ /** Columnar payload unwrapped from the per-cycle envelope, or the
19
+ * blob itself for legacy flat shapes. `null` until the fetch lands. */
20
+ raw: Record<string, number[]> | null;
21
+ /** Full envelope as returned by `tis.read_raw`. Carries
22
+ * `cycle_index`, `cycle_fields`, and `context` alongside `data`. */
23
+ envelope: any | null;
24
+ loading: boolean;
25
+ error: string | null;
26
+ }
27
+ export declare function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataResult;
28
+ /**
29
+ * `tis.read_raw` returns one of:
30
+ * - per-cycle envelope: `{ cycle_index, cycle_fields, context, data: { col: number[] } }`
31
+ * - legacy flat blob: `{ col: number[] }`
32
+ *
33
+ * Chart and CSV code only care about the columnar payload — peel off
34
+ * the envelope when present, otherwise pass the blob through. Returned
35
+ * shape is always `Record<string, number[]>` so consumers can index
36
+ * uniformly.
37
+ */
38
+ export declare function unwrapEnvelope(blob: any): Record<string, number[]>;
39
+ //# sourceMappingURL=useRawCycleData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRawCycleData.d.ts","sourceRoot":"","sources":["../../../src/components/tis/useRawCycleData.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,sBAAsB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAG,MAAM,CAAC;IACnB,KAAK,CAAC,EAAM,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,EAAI,MAAM,CAAC;IACnB;;sDAEkD;IAClD,OAAO,EAAK,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IAClC;kEAC8D;IAC9D,MAAM,EAAY,MAAM,EAAE,CAAC;IAC3B,aAAa,EAAK,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C;4EACwE;IACxE,GAAG,EAAe,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC;IAClD;yEACqE;IACrE,QAAQ,EAAU,GAAG,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAW,OAAO,CAAC;IAC1B,KAAK,EAAa,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,qBAAqB,CAuFnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAOlE"}
@@ -0,0 +1 @@
1
+ import{useContext,useEffect,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";export function useRawCycleData(e){const{projectId:t,methodId:n,runId:a,blobName:r,enabled:s}=e,{invoke:l}=useContext(EventEmitterContext),[u,o]=useState([]),[c,i]=useState(null),[d,f]=useState(null),[m,y]=useState(null),[p,E]=useState(!1),[_,g]=useState(null);return useEffect(()=>{o([]),i(null),f(null),y(null),g(null)},[t,n,a,r]),useEffect(()=>{if(!(s&&t&&n&&a))return;let e=!1;return(async()=>{try{const s=await l("tis.list_raw",MessageType.Request,{project_id:t,method_id:n,run_id:a});if(e||!s?.success)return;const u=(s.data?.cycles??[]).filter(e=>e?.name===r&&"number"==typeof e?.cycle_index).map(e=>e.cycle_index).sort((e,t)=>e-t);o(u),u.length>0&&i(e=>e??u[u.length-1])}catch{}})(),()=>{e=!0}},[s,t,n,a,r,l]),useEffect(()=>{if(!(s&&t&&n&&a))return f(null),y(null),E(!1),void g(null);let e=!1;return E(!0),g(null),(async()=>{try{const s={project_id:t,method_id:n,run_id:a,name:r};null!=c&&(s.cycle_index=c);const u=await l("tis.read_raw",MessageType.Request,s);if(e)return;if(u?.success){const e=u.data??{};y(e),f(unwrapEnvelope(e))}else g(u?.error_message??"Failed to read raw data")}catch(t){e||g(String(t?.message??t))}finally{e||E(!1)}})(),()=>{e=!0}},[s,t,n,a,r,c,l]),{cycles:u,selectedCycle:c,setSelectedCycle:i,raw:d,envelope:m,loading:p,error:_}}export function unwrapEnvelope(e){return e&&"object"==typeof e?"data"in e&&e.data&&"object"==typeof e.data&&Object.values(e.data).some(e=>Array.isArray(e))?e.data:e:{}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.82",
3
+ "version": "3.3.83",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -2,10 +2,26 @@
2
2
  * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
3
  *
4
4
  * TestDataView — standardized test-detail view for the Test Information
5
- * System. Renders metadata header + cycle-scatter chart + virtual-scroll
6
- * cycle table + results table, and subscribes to live `tis.cycle_added`
7
- * and `tis.results_updated` broadcasts so the display updates as the
8
- * control program appends cycles.
5
+ * System. Renders, top-to-bottom:
6
+ *
7
+ * - metadata header (sample, project/method/run, "View Raw Data" btn)
8
+ * - **unified chart panel**: one dropdown lists every view declared in
9
+ * the schema (any type). The chart area dispatches on `view.type`:
10
+ * * `raw_trace` — plots columns from the per-cycle raw blob,
11
+ * shows a cycle picker when >1 cycle exists.
12
+ * * `cycle_scatter` — plots per-cycle scalars across the full run.
13
+ * Raw blob fetching is lazy: nothing's pulled until a raw_trace view
14
+ * is actually selected, so scatter-only runs don't pay the round trip.
15
+ * - virtual-scroll cycle table
16
+ * - results table
17
+ *
18
+ * Subscribes to live `tis.cycle_added` and `tis.results_updated`
19
+ * broadcasts so cycle data + scatter chart update as the control
20
+ * program appends cycles.
21
+ *
22
+ * Sibling <TestRawDataView> stays exported for callers that want a
23
+ * focused trace-only viewer with no scatter / cycle-table / results
24
+ * sections.
9
25
  */
10
26
 
11
27
  import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
@@ -26,6 +42,7 @@ import { Line } from 'react-chartjs-2';
26
42
  import { EventEmitterContext } from '../../core/EventEmitterContext';
27
43
  import { MessageType } from '../../hub/CommandMessage';
28
44
  import { useTis } from './TisProvider';
45
+ import { useRawCycleData } from './useRawCycleData';
29
46
 
30
47
  ChartJS.register(
31
48
  CategoryScale, LinearScale, PointElement, LineElement,
@@ -88,6 +105,9 @@ export interface TestDataViewProps {
88
105
  throttleMs?: number;
89
106
  /** Fixed cycle-table scroll height. Default "400px". */
90
107
  cycleTableHeight?: string;
108
+ /** Height of the unified chart panel (any CSS length). Default "320px".
109
+ * Set to e.g. "50vh" for a taller chart on a single-test page. */
110
+ chartHeight?: string;
91
111
  }
92
112
 
93
113
  // -------------------------------------------------------------------------
@@ -98,7 +118,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
98
118
  const methodId = props.methodId ?? tis.selection.methodId;
99
119
  const runId = props.runId ?? tis.selection.runId;
100
120
  const schema = props.schema ?? (methodId ? (tis.schemas[methodId] as TestMethod) : undefined);
101
- const { throttleMs = 100, cycleTableHeight = '400px' } = props;
121
+ const { throttleMs = 100, cycleTableHeight = '400px', chartHeight = '320px' } = props;
102
122
  const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
103
123
 
104
124
  const [meta, setMeta] = useState<any>(null);
@@ -128,37 +148,42 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
128
148
  const [availableCycles, setAvailableCycles] = useState<number[]>([]);
129
149
  const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
130
150
 
131
- // Scatter-capable views only raw_trace lives in <TestRawDataView>.
132
- const scatterViews = useMemo(() => {
151
+ // All views, any type, in declaration order. The dropdown lists
152
+ // every one; the chart area dispatches on `view.type` to render
153
+ // either a scatter or a raw_trace chart from the appropriate data.
154
+ const allViews = useMemo(() => {
133
155
  const out: { name: string; view: ChartView }[] = [];
134
156
  for (const [name, v] of Object.entries(schema?.views ?? {})) {
135
- if ((v as ChartView).type === 'cycle_scatter') out.push({ name, view: v as ChartView });
157
+ out.push({ name, view: v as ChartView });
136
158
  }
137
159
  return out;
138
160
  }, [schema]);
139
161
 
140
162
  const [selectedView, setSelectedView] = useState<string | null>(
141
- scatterViews.length > 0 ? scatterViews[0].name : null,
163
+ allViews.length > 0 ? allViews[0].name : null,
142
164
  );
143
165
 
144
166
  // Default to the first available view as soon as the schema loads.
145
167
  // The useState initializer above only runs on first mount, when
146
- // scatterViews is typically still empty (schema fetch in flight).
168
+ // allViews is typically still empty (schema fetch in flight).
147
169
  // Without this effect, selectedView stays null and the chart
148
170
  // stays blank until the operator opens the dropdown — almost
149
171
  // nobody does, so they email asking why the chart is broken.
150
172
  //
151
173
  // Also handles the case where the schema changes and the
152
- // currently-selected view is no longer in scatterViews — falls
153
- // back to the new first view rather than rendering nothing.
174
+ // currently-selected view is no longer in allViews — falls back
175
+ // to the new first view rather than rendering nothing.
154
176
  useEffect(() => {
155
- if (scatterViews.length === 0) return;
177
+ if (allViews.length === 0) return;
156
178
  const stillValid = selectedView !== null
157
- && scatterViews.some(v => v.name === selectedView);
179
+ && allViews.some(v => v.name === selectedView);
158
180
  if (!stillValid) {
159
- setSelectedView(scatterViews[0].name);
181
+ setSelectedView(allViews[0].name);
160
182
  }
161
- }, [scatterViews, selectedView]);
183
+ }, [allViews, selectedView]);
184
+
185
+ const selectedViewDef = allViews.find(v => v.name === selectedView)?.view;
186
+ const isRawTraceView = selectedViewDef?.type === 'raw_trace';
162
187
 
163
188
  // Pending updates coalesced by a throttle window — keeps React
164
189
  // re-renders at <= 1 / throttleMs even if cycles stream faster.
@@ -245,58 +270,102 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
245
270
  }, [projectId, methodId, runId, throttleMs]);
246
271
 
247
272
  // -----------------------------------------------------------------
248
- // Chart data
273
+ // Raw-trace data fetch (lazy)
274
+ //
275
+ // Only ever pulls a blob when the active chart view is a raw_trace.
276
+ // Switching to a cycle_scatter view leaves the previously-loaded
277
+ // raw state alone (still in memory but unused), so flipping back
278
+ // and forth is instant after the first fetch.
279
+ // -----------------------------------------------------------------
280
+ const traceBlobName = schema?.raw_data?.blob_name ?? 'trace';
281
+ const traceFetch = useRawCycleData({
282
+ projectId, methodId, runId,
283
+ blobName: traceBlobName,
284
+ enabled: isRawTraceView,
285
+ });
286
+
287
+ // -----------------------------------------------------------------
288
+ // Chart data — dispatches on view.type so one panel handles both
289
+ // shapes. Returns null when the active view's input data isn't
290
+ // ready (e.g., raw blob still loading); the render block treats
291
+ // null as "show overlay instead of an empty chart."
249
292
  // -----------------------------------------------------------------
250
293
  const chartData = useMemo(() => {
251
- if (!selectedView || scatterViews.length === 0) return null;
252
- const view = scatterViews.find(v => v.name === selectedView)?.view;
253
- if (!view) return null;
254
-
255
- const xField = view.x.field!;
256
- const asc = [...cycles].reverse(); // cycles state is newest-first; charts want oldest-first
257
- const xs = asc.map(c => c[xField]);
258
-
259
- const datasets = view.y.map((s, idx) => ({
260
- label: s.label ?? s.field,
261
- data: asc.map(c => c[s.field!]),
262
- yAxisID: s.y_axis === 'right' ? 'y1' : 'y',
263
- borderColor: palette(idx),
264
- backgroundColor: palette(idx),
265
- tension: 0.1,
266
- pointRadius: 2,
267
- }));
268
-
269
- return { labels: xs, datasets };
270
- }, [cycles, selectedView, scatterViews]);
271
-
272
- const selectedViewDef = scatterViews.find(v => v.name === selectedView)?.view;
294
+ if (!selectedViewDef) return null;
295
+ if (selectedViewDef.type === 'cycle_scatter') {
296
+ const xField = selectedViewDef.x.field;
297
+ if (!xField) return null;
298
+ const asc = [...cycles].reverse(); // state is newest-first; charts want oldest-first
299
+ const xs = asc.map(c => c[xField]);
300
+ const datasets = selectedViewDef.y.map((s, idx) => ({
301
+ label: s.label ?? s.field,
302
+ data: asc.map(c => c[s.field!]),
303
+ yAxisID: s.y_axis === 'right' ? 'y1' : 'y',
304
+ borderColor: palette(idx),
305
+ backgroundColor: palette(idx),
306
+ tension: 0.1,
307
+ pointRadius: 2,
308
+ }));
309
+ return { labels: xs, datasets };
310
+ }
311
+ if (selectedViewDef.type === 'raw_trace') {
312
+ if (!traceFetch.raw) return null;
313
+ const xCol = selectedViewDef.x.column;
314
+ if (!xCol) return null;
315
+ const xs = traceFetch.raw[xCol] ?? [];
316
+ const datasets = selectedViewDef.y.map((s, idx) => ({
317
+ label: s.label ?? s.column,
318
+ data: (traceFetch.raw![s.column!] ?? []).map((y, i) => ({ x: xs[i], y })),
319
+ yAxisID: s.y_axis === 'right' ? 'y1' : 'y',
320
+ borderColor: palette(idx),
321
+ backgroundColor: palette(idx),
322
+ pointRadius: 0,
323
+ borderWidth: 1.5,
324
+ showLine: true,
325
+ }));
326
+ return { datasets };
327
+ }
328
+ return null;
329
+ }, [selectedViewDef, cycles, traceFetch.raw]);
330
+
273
331
  const usesRightAxis = selectedViewDef?.y.some(s => s.y_axis === 'right') ?? false;
274
332
 
275
- const chartOptions = useMemo(() => ({
276
- responsive: true,
277
- maintainAspectRatio: false,
278
- scales: {
279
- x: { title: { display: !!selectedViewDef?.x.label, text: selectedViewDef?.x.label } },
280
- y: { position: 'left' as const,
281
- title: { display: true, text: leftAxisLabel(selectedViewDef) } },
282
- ...(usesRightAxis ? {
283
- y1: { position: 'right' as const,
284
- grid: { drawOnChartArea: false },
285
- title: { display: true, text: rightAxisLabel(selectedViewDef) } },
286
- } : {}),
287
- },
288
- plugins: {
289
- legend: { display: true },
290
- zoom: {
291
- pan: { enabled: true, mode: 'xy' as const },
333
+ const chartOptions = useMemo(() => {
334
+ const isTrace = selectedViewDef?.type === 'raw_trace';
335
+ return {
336
+ responsive: true,
337
+ maintainAspectRatio: false,
338
+ // raw_trace datasets are pre-built `{x, y}` points so
339
+ // chart.js shouldn't try to parse them; cycle_scatter uses
340
+ // the `labels` + per-dataset `data: number[]` shape and
341
+ // needs default parsing on.
342
+ parsing: isTrace ? (false as const) : undefined,
343
+ scales: {
344
+ x: isTrace
345
+ ? { type: 'linear' as const,
346
+ title: { display: !!selectedViewDef?.x.label, text: selectedViewDef?.x.label } }
347
+ : { title: { display: !!selectedViewDef?.x.label, text: selectedViewDef?.x.label } },
348
+ y: { position: 'left' as const,
349
+ title: { display: true, text: leftAxisLabel(selectedViewDef) } },
350
+ ...(usesRightAxis ? {
351
+ y1: { position: 'right' as const,
352
+ grid: { drawOnChartArea: false },
353
+ title: { display: true, text: rightAxisLabel(selectedViewDef) } },
354
+ } : {}),
355
+ },
356
+ plugins: {
357
+ legend: { display: true },
292
358
  zoom: {
293
- wheel: { enabled: true },
294
- pinch: { enabled: true },
295
- mode: 'xy' as const,
359
+ pan: { enabled: true, mode: 'xy' as const },
360
+ zoom: {
361
+ wheel: { enabled: true },
362
+ pinch: { enabled: true },
363
+ mode: 'xy' as const,
364
+ },
296
365
  },
297
366
  },
298
- },
299
- }), [selectedViewDef, usesRightAxis]);
367
+ };
368
+ }, [selectedViewDef, usesRightAxis]);
300
369
 
301
370
  // -----------------------------------------------------------------
302
371
  // View Raw Data dialog: lazy-fetch raw + filtered blobs the first
@@ -439,17 +508,41 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
439
508
  affordance instead of silence. The dropdown is disabled
440
509
  in that case; the chart area renders empty. */}
441
510
  <div className="p-card" style={{ padding: '1rem' }}>
442
- <div className="flex" style={{ gap: '1rem', alignItems: 'center', marginBottom: '0.5rem' }}>
511
+ <div className="flex" style={{ gap: '1rem', alignItems: 'center', marginBottom: '0.5rem', flexWrap: 'wrap' }}>
443
512
  <Dropdown
444
513
  value={selectedView}
445
- options={scatterViews.map(v => ({ label: v.view.title ?? v.name, value: v.name }))}
514
+ options={allViews.map(v => ({ label: v.view.title ?? v.name, value: v.name }))}
446
515
  onChange={(e) => setSelectedView(e.value)}
447
- placeholder={scatterViews.length === 0 ? 'No view defined' : 'Select a view'}
448
- disabled={scatterViews.length === 0}
516
+ placeholder={allViews.length === 0 ? 'No view defined' : 'Select a view'}
517
+ disabled={allViews.length === 0}
449
518
  />
450
519
  <h3 style={{ margin: 0 }}>{selectedViewDef?.title ?? ''}</h3>
520
+ {/* Cycle picker — only visible for raw_trace views
521
+ AND when more than one cycle exists. The hook
522
+ handles the cycle-list discovery and "default
523
+ to latest" behaviour so this stays declarative. */}
524
+ {isRawTraceView && traceFetch.cycles.length > 1 && (
525
+ <>
526
+ <label htmlFor="chart-cycle-picker"
527
+ style={{ color: 'var(--text-secondary-color)' }}>Cycle:</label>
528
+ <Dropdown
529
+ inputId="chart-cycle-picker"
530
+ value={traceFetch.selectedCycle}
531
+ options={traceFetch.cycles.map(c => ({ label: `Cycle ${c}`, value: c }))}
532
+ onChange={(e) => traceFetch.setSelectedCycle(Number(e.value))}
533
+ style={{ minWidth: '8rem' }}
534
+ />
535
+ <span style={{ color: 'var(--text-secondary-color)' }}>
536
+ of {traceFetch.cycles.length}
537
+ </span>
538
+ </>
539
+ )}
451
540
  </div>
452
- <div style={{ height: 320 }}>
541
+ <div style={{ height: chartHeight, position: 'relative' }}>
542
+ {isRawTraceView && traceFetch.loading &&
543
+ <ChartOverlay>Loading raw data…</ChartOverlay>}
544
+ {isRawTraceView && traceFetch.error &&
545
+ <ChartOverlay>{traceFetch.error}</ChartOverlay>}
453
546
  {chartData && <Line data={chartData} options={chartOptions} />}
454
547
  </div>
455
548
  </div>
@@ -864,10 +957,25 @@ const CHART_COLORS = [
864
957
  ];
865
958
  const palette = (i: number) => CHART_COLORS[i % CHART_COLORS.length];
866
959
 
960
+ // Loading / error wash drawn over the chart area while a raw_trace
961
+ // fetch is in flight. Centered, pointer-events-none so the operator
962
+ // can still interact with the dropdown above.
963
+ const ChartOverlay: React.FC<{ children: React.ReactNode }> = ({ children }) => (
964
+ <div style={{ position: 'absolute', inset: 0, display: 'flex',
965
+ alignItems: 'center', justifyContent: 'center',
966
+ color: 'var(--text-secondary-color)', pointerEvents: 'none' }}>
967
+ {children}
968
+ </div>
969
+ );
970
+
971
+ // Axis labels work for both scatter (s.field) and raw_trace (s.column)
972
+ // series. Whichever the view declared, that's what's used as a label
973
+ // fallback when the explicit `label` is absent.
974
+ const seriesLabel = (s: ChartSeries) => s.label ?? s.field ?? s.column ?? '';
867
975
  const leftAxisLabel = (v?: ChartView) =>
868
- v?.y.filter(s => s.y_axis !== 'right').map(s => s.label ?? s.field).join(' / ') ?? '';
976
+ v?.y.filter(s => s.y_axis !== 'right').map(seriesLabel).join(' / ') ?? '';
869
977
  const rightAxisLabel = (v?: ChartView) =>
870
- v?.y.filter(s => s.y_axis === 'right').map(s => s.label ?? s.field).join(' / ') ?? '';
978
+ v?.y.filter(s => s.y_axis === 'right').map(seriesLabel).join(' / ') ?? '';
871
979
 
872
980
  const formatCell = (v: any, type: string): string => {
873
981
  if (v === null || v === undefined) return '';
@@ -10,7 +10,7 @@
10
10
  * built-in dialog inside <TestDataView>.
11
11
  */
12
12
 
13
- import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
13
+ import React, { useMemo, useRef, useState } from 'react';
14
14
  import { Button } from 'primereact/button';
15
15
  import { Dropdown } from 'primereact/dropdown';
16
16
 
@@ -21,10 +21,9 @@ import { Chart as ChartJS,
21
21
  import zoomPlugin from 'chartjs-plugin-zoom';
22
22
  import { Line } from 'react-chartjs-2';
23
23
 
24
- import { EventEmitterContext } from '../../core/EventEmitterContext';
25
- import { MessageType } from '../../hub/CommandMessage';
26
24
  import type { ChartView, TestMethod } from './TestDataView';
27
25
  import { useTis } from './TisProvider';
26
+ import { useRawCycleData } from './useRawCycleData';
28
27
 
29
28
  ChartJS.register(
30
29
  CategoryScale, LinearScale, PointElement, LineElement,
@@ -53,19 +52,8 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
53
52
  const runId = props.runId ?? tis.selection.runId;
54
53
  const schema = props.schema ?? (methodId ? (tis.schemas[methodId] as TestMethod) : undefined);
55
54
  const { blobName, chartHeight = '60vh' } = props;
56
- const { invoke } = useContext(EventEmitterContext);
57
-
58
- const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
59
- const [envelope, setEnvelope] = useState<any | null>(null);
60
- const [loading, setLoading] = useState(true);
61
- const [error, setError] = useState<string | null>(null);
62
55
  const chartRef = useRef<any>(null);
63
56
 
64
- // Per-test cycle picker. Default to latest cycle on disk; the
65
- // operator can flip backward through earlier cycles.
66
- const [cycles, setCycles] = useState<number[]>([]);
67
- const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
68
-
69
57
  // raw_trace-capable views only — cycle scatter lives in <TestDataView>.
70
58
  const traceViews = useMemo(() => {
71
59
  const out: { name: string; view: ChartView }[] = [];
@@ -81,75 +69,19 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
81
69
 
82
70
  const effectiveBlobName = blobName ?? schema?.raw_data?.blob_name ?? 'trace';
83
71
 
84
- // Reset cycle state when the run identity changes; the new run has
85
- // its own cycle list and "latest" target.
86
- useEffect(() => {
87
- setCycles([]);
88
- setSelectedCycle(null);
89
- }, [projectId, methodId, runId, effectiveBlobName]);
90
-
91
- // Discover available cycles for this run/blob. Runs ahead of the
92
- // data fetch so the cycle picker can render immediately.
93
- useEffect(() => {
94
- if (!projectId || !methodId || !runId) return;
95
- let cancelled = false;
96
- (async () => {
97
- try {
98
- const resp: any = await invoke(
99
- 'tis.list_raw' as any, MessageType.Request as any,
100
- { project_id: projectId, method_id: methodId, run_id: runId } as any);
101
- if (cancelled || !resp?.success) return;
102
- const list: any[] = resp.data?.cycles ?? [];
103
- const indices = list
104
- .filter(c => c?.name === effectiveBlobName && typeof c?.cycle_index === 'number')
105
- .map(c => c.cycle_index as number)
106
- .sort((a, b) => a - b);
107
- setCycles(indices);
108
- if (indices.length > 0) {
109
- setSelectedCycle(prev => prev ?? indices[indices.length - 1]);
110
- }
111
- } catch {
112
- // Listing failure is non-fatal — the data fetch below
113
- // still tries "latest" and the picker just stays empty.
114
- }
115
- })();
116
- return () => { cancelled = true; };
117
- }, [projectId, methodId, runId, effectiveBlobName, invoke]);
118
-
119
- // Lazy fetch — runs on mount / when identifiers / selectedCycle change.
120
- useEffect(() => {
121
- if (!projectId || !methodId || !runId) {
122
- setRaw(null); setEnvelope(null); setLoading(false); setError(null);
123
- return;
124
- }
125
- let cancelled = false;
126
- setLoading(true);
127
- setError(null);
128
- (async () => {
129
- try {
130
- const args: Record<string, any> = {
131
- project_id: projectId, method_id: methodId,
132
- run_id: runId, name: effectiveBlobName,
133
- };
134
- if (selectedCycle != null) args.cycle_index = selectedCycle;
135
- const resp: any = await invoke(
136
- 'tis.read_raw' as any, MessageType.Request as any, args as any);
137
- if (cancelled) return;
138
- if (resp?.success) {
139
- const payload = resp.data ?? {};
140
- setEnvelope(payload);
141
- setRaw(unwrapEnvelope(payload));
142
- } else {
143
- setError(resp?.error_message ?? 'Failed to read raw data');
144
- }
145
- } catch (e: any) {
146
- if (!cancelled) setError(String(e?.message ?? e));
147
- } finally {
148
- if (!cancelled) setLoading(false);
149
- }
150
- })();
151
- return () => { cancelled = true; };
152
- }, [projectId, methodId, runId, effectiveBlobName, selectedCycle, invoke]);
72
+ // Cycle discovery + per-cycle blob fetch live in the shared hook so
73
+ // <TestDataView>'s unified panel can reuse the exact same fetch path
74
+ // when a raw_trace view is selected there.
75
+ const { cycles, selectedCycle, setSelectedCycle, raw, envelope, loading, error } =
76
+ useRawCycleData({
77
+ projectId, methodId, runId,
78
+ blobName: effectiveBlobName,
79
+ // This component only renders raw_trace charts, so always
80
+ // fetch as long as the run identity is in scope. The early
81
+ // returns below cover the "no test selected" / "no schema"
82
+ // cases that would otherwise be wasted requests.
83
+ enabled: !!projectId && !!methodId && !!runId,
84
+ });
153
85
 
154
86
  const chartData = useMemo(() => {
155
87
  if (!raw || !selectedView) return null;
@@ -279,20 +211,6 @@ const EmptyState: React.FC<{ message: string }> = ({ message }) => (
279
211
  <div style={{ padding: '1rem', color: 'var(--text-secondary-color)' }}>{message}</div>
280
212
  );
281
213
 
282
- // tis.read_raw returns one of:
283
- // - per-cycle envelope: { cycle_index, cycle_fields, context, data }
284
- // - legacy flat blob: { col: number[] }
285
- // Chart/CSV code only wants the columnar payload — strip the envelope
286
- // when present, otherwise pass the blob through.
287
- const unwrapEnvelope = (blob: any): Record<string, number[]> => {
288
- if (!blob || typeof blob !== 'object') return {};
289
- if ('data' in blob && blob.data && typeof blob.data === 'object'
290
- && Object.values(blob.data).some(v => Array.isArray(v))) {
291
- return blob.data;
292
- }
293
- return blob;
294
- };
295
-
296
214
  // Strip of cycle_index + cycle_fields + context, rendered above the
297
215
  // chart so the operator can see *which* cycle they're looking at and
298
216
  // what schema-declared metric values were active for it. Renders
@@ -0,0 +1,157 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * useRawCycleData — shared hook that fetches `tis.list_raw` + `tis.read_raw`
5
+ * for one (project, method, run, blob_name) slice and tracks the
6
+ * operator's per-cycle selection. Used by:
7
+ *
8
+ * - <TestRawDataView> — the focused trace-only viewer.
9
+ * - <TestDataView> — the unified chart panel, which lazily fetches
10
+ * raw data only when a raw_trace view is selected
11
+ * (set `enabled` to false to short-circuit while
12
+ * a cycle_scatter view is active).
13
+ *
14
+ * Keeping the fetch logic in one place means the two components can't
15
+ * drift in how they discover cycles, default to "latest cycle", or
16
+ * unwrap the per-cycle envelope.
17
+ */
18
+
19
+ import { useContext, useEffect, useState } from 'react';
20
+ import { EventEmitterContext } from '../../core/EventEmitterContext';
21
+ import { MessageType } from '../../hub/CommandMessage';
22
+
23
+ export interface UseRawCycleDataOptions {
24
+ projectId?: string;
25
+ methodId?: string;
26
+ runId?: string;
27
+ /** Blob name (e.g. "trace"); usually schema.raw_data.blob_name. */
28
+ blobName: string;
29
+ /** When false, the hook does no fetching and returns empty state.
30
+ * Wire to "is the current chart view a raw_trace?" so scatter
31
+ * selections don't trigger a blob round-trip. */
32
+ enabled: boolean;
33
+ }
34
+
35
+ export interface UseRawCycleDataResult {
36
+ /** Per-cycle indices discovered on disk for this run/blob, sorted
37
+ * ascending. Empty when no raw_data has been written yet. */
38
+ cycles: number[];
39
+ selectedCycle: number | null;
40
+ setSelectedCycle: (n: number | null) => void;
41
+ /** Columnar payload unwrapped from the per-cycle envelope, or the
42
+ * blob itself for legacy flat shapes. `null` until the fetch lands. */
43
+ raw: Record<string, number[]> | null;
44
+ /** Full envelope as returned by `tis.read_raw`. Carries
45
+ * `cycle_index`, `cycle_fields`, and `context` alongside `data`. */
46
+ envelope: any | null;
47
+ loading: boolean;
48
+ error: string | null;
49
+ }
50
+
51
+ export function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataResult {
52
+ const { projectId, methodId, runId, blobName, enabled } = opts;
53
+ const { invoke } = useContext(EventEmitterContext);
54
+
55
+ const [cycles, setCycles] = useState<number[]>([]);
56
+ const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
57
+ const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
58
+ const [envelope, setEnvelope] = useState<any | null>(null);
59
+ const [loading, setLoading] = useState(false);
60
+ const [error, setError] = useState<string | null>(null);
61
+
62
+ // Reset cycle state when the run identity (or blob) changes — the
63
+ // new slice has its own cycle list and its own "latest" target.
64
+ useEffect(() => {
65
+ setCycles([]);
66
+ setSelectedCycle(null);
67
+ setRaw(null);
68
+ setEnvelope(null);
69
+ setError(null);
70
+ }, [projectId, methodId, runId, blobName]);
71
+
72
+ // Cycle-list discovery. Runs ahead of the data fetch so the cycle
73
+ // picker can render immediately. Filtered to the requested blob name.
74
+ useEffect(() => {
75
+ if (!enabled || !projectId || !methodId || !runId) return;
76
+ let cancelled = false;
77
+ (async () => {
78
+ try {
79
+ const resp: any = await invoke(
80
+ 'tis.list_raw' as any, MessageType.Request as any,
81
+ { project_id: projectId, method_id: methodId, run_id: runId } as any,
82
+ );
83
+ if (cancelled || !resp?.success) return;
84
+ const list: any[] = resp.data?.cycles ?? [];
85
+ const indices = list
86
+ .filter(c => c?.name === blobName && typeof c?.cycle_index === 'number')
87
+ .map(c => c.cycle_index as number)
88
+ .sort((a, b) => a - b);
89
+ setCycles(indices);
90
+ if (indices.length > 0) {
91
+ setSelectedCycle(prev => prev ?? indices[indices.length - 1]);
92
+ }
93
+ } catch {
94
+ // Listing failure is non-fatal — the data fetch below
95
+ // still tries "latest" and the picker just stays empty.
96
+ }
97
+ })();
98
+ return () => { cancelled = true; };
99
+ }, [enabled, projectId, methodId, runId, blobName, invoke]);
100
+
101
+ // Lazy blob fetch — runs whenever identifiers / selectedCycle change.
102
+ useEffect(() => {
103
+ if (!enabled || !projectId || !methodId || !runId) {
104
+ setRaw(null); setEnvelope(null); setLoading(false); setError(null);
105
+ return;
106
+ }
107
+ let cancelled = false;
108
+ setLoading(true);
109
+ setError(null);
110
+ (async () => {
111
+ try {
112
+ const args: Record<string, any> = {
113
+ project_id: projectId, method_id: methodId,
114
+ run_id: runId, name: blobName,
115
+ };
116
+ if (selectedCycle != null) args.cycle_index = selectedCycle;
117
+ const resp: any = await invoke(
118
+ 'tis.read_raw' as any, MessageType.Request as any, args as any,
119
+ );
120
+ if (cancelled) return;
121
+ if (resp?.success) {
122
+ const payload = resp.data ?? {};
123
+ setEnvelope(payload);
124
+ setRaw(unwrapEnvelope(payload));
125
+ } else {
126
+ setError(resp?.error_message ?? 'Failed to read raw data');
127
+ }
128
+ } catch (e: any) {
129
+ if (!cancelled) setError(String(e?.message ?? e));
130
+ } finally {
131
+ if (!cancelled) setLoading(false);
132
+ }
133
+ })();
134
+ return () => { cancelled = true; };
135
+ }, [enabled, projectId, methodId, runId, blobName, selectedCycle, invoke]);
136
+
137
+ return { cycles, selectedCycle, setSelectedCycle, raw, envelope, loading, error };
138
+ }
139
+
140
+ /**
141
+ * `tis.read_raw` returns one of:
142
+ * - per-cycle envelope: `{ cycle_index, cycle_fields, context, data: { col: number[] } }`
143
+ * - legacy flat blob: `{ col: number[] }`
144
+ *
145
+ * Chart and CSV code only care about the columnar payload — peel off
146
+ * the envelope when present, otherwise pass the blob through. Returned
147
+ * shape is always `Record<string, number[]>` so consumers can index
148
+ * uniformly.
149
+ */
150
+ export function unwrapEnvelope(blob: any): Record<string, number[]> {
151
+ if (!blob || typeof blob !== 'object') return {};
152
+ if ('data' in blob && blob.data && typeof blob.data === 'object'
153
+ && Object.values(blob.data).some(v => Array.isArray(v))) {
154
+ return blob.data;
155
+ }
156
+ return blob;
157
+ }