@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.
- package/dist/components/tis/TestDataView.d.ts +3 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
- package/dist/components/tis/TestRawDataView.js +1 -1
- package/dist/components/tis/useRawCycleData.d.ts +39 -0
- package/dist/components/tis/useRawCycleData.d.ts.map +1 -0
- package/dist/components/tis/useRawCycleData.js +1 -0
- package/package.json +1 -1
- package/src/components/tis/TestDataView.tsx +176 -68
- package/src/components/tis/TestRawDataView.tsx +15 -97
- package/src/components/tis/useRawCycleData.ts +157 -0
|
@@ -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":"
|
|
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,
|
|
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,{
|
|
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
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
//
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
153
|
-
//
|
|
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 (
|
|
177
|
+
if (allViews.length === 0) return;
|
|
156
178
|
const stillValid = selectedView !== null
|
|
157
|
-
&&
|
|
179
|
+
&& allViews.some(v => v.name === selectedView);
|
|
158
180
|
if (!stillValid) {
|
|
159
|
-
setSelectedView(
|
|
181
|
+
setSelectedView(allViews[0].name);
|
|
160
182
|
}
|
|
161
|
-
}, [
|
|
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
|
-
//
|
|
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 (!
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
}
|
|
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={
|
|
514
|
+
options={allViews.map(v => ({ label: v.view.title ?? v.name, value: v.name }))}
|
|
446
515
|
onChange={(e) => setSelectedView(e.value)}
|
|
447
|
-
placeholder={
|
|
448
|
-
disabled={
|
|
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:
|
|
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(
|
|
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(
|
|
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, {
|
|
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
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
}
|