@adcops/autocore-react 3.3.73 → 3.3.75

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.
@@ -0,0 +1,4 @@
1
+ import type { SVGProps } from "react";
2
+ export declare const HomeMotor: (props: SVGProps<SVGSVGElement>) => import("react/jsx-runtime").JSX.Element;
3
+ export default HomeMotor;
4
+ //# sourceMappingURL=HomeMotor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HomeMotor.d.ts","sourceRoot":"","sources":["../../src/assets/HomeMotor.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,eAAO,MAAM,SAAS,GAAI,OAAO,QAAQ,CAAC,aAAa,CAAC,4CAgCvD,CAAC;AAEF,eAAe,SAAS,CAAC"}
@@ -0,0 +1 @@
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";export const HomeMotor=o=>_jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",...o,children:[_jsx("path",{d:"M8.5 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-2"}),_jsx("circle",{cx:"12",cy:"12.5",r:"4"}),_jsx("circle",{cx:"12",cy:"12.5",r:"1",fill:"currentColor",stroke:"none"})]});export default HomeMotor;
@@ -0,0 +1,57 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ class="lucide lucide-house-plug-icon lucide-house-plug"
12
+ version="1.1"
13
+ id="svg4"
14
+ sodipodi:docname="home-motor.svg"
15
+ inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
16
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
17
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ xmlns:svg="http://www.w3.org/2000/svg">
20
+ <defs
21
+ id="defs4" />
22
+ <sodipodi:namedview
23
+ id="namedview4"
24
+ pagecolor="#ffffff"
25
+ bordercolor="#000000"
26
+ borderopacity="0.25"
27
+ inkscape:showpageshadow="2"
28
+ inkscape:pageopacity="0.0"
29
+ inkscape:pagecheckerboard="0"
30
+ inkscape:deskcolor="#d1d1d1"
31
+ inkscape:zoom="50.541667"
32
+ inkscape:cx="12"
33
+ inkscape:cy="12"
34
+ inkscape:window-width="2560"
35
+ inkscape:window-height="1494"
36
+ inkscape:window-x="-11"
37
+ inkscape:window-y="-11"
38
+ inkscape:window-maximized="1"
39
+ inkscape:current-layer="svg4" />
40
+ <path
41
+ d="M8.5 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-2"
42
+ id="path4" />
43
+ <ellipse
44
+ style="fill:none;stroke-width:2.08003;stroke-dasharray:none"
45
+ id="path5"
46
+ cx="11.970321"
47
+ cy="12.366035"
48
+ rx="3.9292588"
49
+ ry="3.9292586" />
50
+ <ellipse
51
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.98654;stroke-dasharray:none;stroke-opacity:1"
52
+ id="path6"
53
+ cx="11.970321"
54
+ cy="12.366035"
55
+ rx="0.89035445"
56
+ ry="0.89035451" />
57
+ </svg>
@@ -57,7 +57,7 @@ interface ValueInputProps extends Omit<InputNumberProps, 'value' | 'size'> {
57
57
  /**
58
58
  * The label for the ValueInput field.
59
59
  */
60
- label: React.ReactNode | undefined;
60
+ label: React.ReactNode | undefined | null;
61
61
  /**
62
62
  * The value for the field.
63
63
  */
@@ -1 +1 @@
1
- {"version":3,"file":"ValueInput.d.ts","sourceRoot":"","sources":["../../src/components/ValueInput.tsx"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAGH,OAAO,KAAkD,MAAM,OAAO,CAAC;AAEvE,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAI5E,OAAO,kBAAkB,CAAC;AAE1B,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1D;;GAEG;AACH,UAAU,eAAgB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,CAAC;IAEtE;;OAEG;IACH,KAAK,EAAE,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC;IAGnC;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAErB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAGlC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAGlC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAG1C;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAG1B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5B;;OAEG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC;IAE1C;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAE/B,uGAAuG;IACvG,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEnC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEjC;;;OAGG;IACH,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAmLhD,CAAC;AAEF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"ValueInput.d.ts","sourceRoot":"","sources":["../../src/components/ValueInput.tsx"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAGH,OAAO,KAAkD,MAAM,OAAO,CAAC;AAEvE,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAI5E,OAAO,kBAAkB,CAAC;AAE1B,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1D;;GAEG;AACH,UAAU,eAAgB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,CAAC;IAEtE;;OAEG;IACH,KAAK,EAAE,KAAK,CAAC,SAAS,GAAG,SAAS,GAAG,IAAI,CAAC;IAG1C;;OAEG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAErB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAGlC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAGlC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAG1C;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAG1B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE5B;;OAEG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC;IAE1C;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAE/B,uGAAuG;IACvG,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEnC;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAEjC;;;OAGG;IACH,cAAc,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAmLhD,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ResultHistoryTable.d.ts","sourceRoot":"","sources":["../../../src/components/tis/ResultHistoryTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAQ/D,MAAM,WAAW,uBAAuB;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAwDD,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAqMhE,CAAC"}
1
+ {"version":3,"file":"ResultHistoryTable.d.ts","sourceRoot":"","sources":["../../../src/components/tis/ResultHistoryTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAQ/D,MAAM,WAAW,uBAAuB;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAkHD,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CA4PhE,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Button}from"primereact/button";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";const rawBlobToCsv=e=>{if(!e||"object"!=typeof e)return"";const t=Object.entries(e).filter(([,e])=>Array.isArray(e));if(0===t.length)return"";t.sort(([e],[t])=>"t"===e?-1:"t"===t?1:0);const r=t.map(([e])=>e),o=t.reduce((e,[,t])=>Math.min(e,t.length),1/0),s=e=>{if(null==e)return"";const t=String(e);return/[",\n\r]/.test(t)?`"${t.replace(/"/g,'""')}"`:t},i=[r.join(",")];for(let e=0;e<o;e++)i.push(t.map(([,t])=>s(t[e])).join(","));return i.join("\n")},downloadCsv=(e,t)=>{const r=new Blob([t],{type:"text/csv;charset=utf-8;"}),o=URL.createObjectURL(r),s=document.createElement("a");s.href=o,s.download=e,document.body.appendChild(s),s.click(),s.remove(),URL.revokeObjectURL(o)};export const ResultHistoryTable=e=>{const t=useTis(),r=e.projectId??t.selection.projectId,o=e.methodId,[s,i]=useState([]),[n,a]=useState(!1),[l,d]=useState(null),{invoke:c}=useContext(EventEmitterContext),m=async()=>{if(r){a(!0);try{const e={project_id:r};o&&(e.method_id=o);const t=await c("tis.list_tests",MessageType.Request,e);t.success&&t.data&&t.data.tests&&i(t.data.tests)}catch(e){}a(!1)}else i([])};useEffect(()=>{m()},[r,o,t.state.activeRunId]);const u=async(e,t)=>{const s=e?.run_id,i=e?.method_id??o;if(!s||!i)return;const n="raw"===t?"tis.read_raw":"tis.read_filtered",a="raw"===t?"raw trace":"filtered trace",l="raw"===t?"raw":"filtered";d({runId:s,kind:t});try{const e=await c(n,MessageType.Request,{project_id:r,method_id:i,run_id:s,name:"trace"});if(!e?.success||!e.data)return void alert(`No ${a} available for ${s}`+(e?.error_message?`: ${e.error_message}`:""));const t=rawBlobToCsv(e.data);if(!t)return void alert(`${a} for ${s} is empty or has no array columns.`);downloadCsv(`${r}_${i}_${s}_${l}.csv`,t)}catch(e){alert(`Download failed: ${e instanceof Error?e.message:String(e)}`)}finally{d(null)}},p=e=>{const t="string"==typeof e?.sample_id?e.sample_id:"";if(t)return t;const r=e?.config;return r&&"object"==typeof r&&"string"==typeof r.sample_id?r.sample_id:""};return _jsxs("div",{style:{width:"100%",maxWidth:"100%",overflow:"hidden",boxSizing:"border-box"},children:[_jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"1rem"},children:[_jsx("h3",{style:{margin:0},children:r?`Test History: ${r}${o?` / ${o}`:""}`:"Test History (no project selected)"}),_jsx(Button,{icon:"pi pi-refresh",label:"Refresh",onClick:m,disabled:n})]}),_jsxs(DataTable,{value:s,loading:n,paginator:!0,rows:10,emptyMessage:"No tests found.",scrollable:!0,scrollHeight:"flex",tableStyle:{minWidth:0},style:{width:"100%"},selectionMode:"single",onSelectionChange:e=>{const r=e.value;r?.run_id&&t.setSelection({projectId:r.project_id??null,methodId:r.method_id??null,runId:r.run_id})},children:[_jsx(Column,{header:"Sample ID",sortable:!0,body:p,sortFunction:e=>{const t=[...e.data];return t.sort((t,r)=>p(t).localeCompare(p(r))*(e.order??1)),t},style:{minWidth:"8rem"}}),_jsx(Column,{field:"start_time",header:"Date/Time",sortable:!0,body:e=>{return(t=e.start_time)?new Date(t).toLocaleString():"";var t},style:{minWidth:"12rem"}}),_jsx(Column,{field:"method_id",header:"Test Method",sortable:!0,style:{minWidth:"10rem"}}),_jsx(Column,{field:"run_id",header:"Run ID",sortable:!0,style:{minWidth:"12rem"}}),_jsx(Column,{header:"Download",style:{width:"14rem"},body:e=>{const t=l?.runId===e.run_id&&"raw"===l?.kind,r=l?.runId===e.run_id&&"filtered"===l?.kind,o=null!==l;return _jsxs("div",{style:{display:"flex",gap:"0.4rem"},children:[_jsx(Button,{icon:t?"pi pi-spin pi-spinner":"pi pi-download",label:"Raw",size:"small",outlined:!0,disabled:o,onClick:()=>u(e,"raw"),tooltip:"Download raw_data/trace.json as CSV",tooltipOptions:{position:"left"}}),_jsx(Button,{icon:r?"pi pi-spin pi-spinner":"pi pi-download",label:"Filtered",size:"small",outlined:!0,disabled:o,onClick:()=>u(e,"filtered"),tooltip:"Download filtered_data/trace.json as CSV",tooltipOptions:{position:"left"}})]})}})]})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Button}from"primereact/button";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";const 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:{},escapeCsv=e=>{if(null==e)return"";const t=String(e);return/[",\n\r]/.test(t)?`"${t.replace(/"/g,'""')}"`:t},unifyColumns=e=>{const t=new Set,r=[];for(const s of e)for(const[e,o]of Object.entries(s))Array.isArray(o)&&!t.has(e)&&(t.add(e),r.push(e));return r.sort((e,t)=>"t"===e?-1:"t"===t?1:0),r},cyclesToCsv=e=>{if(0===e.length)return"";const t=e.map(e=>({cycleIndex:e.cycleIndex,blob:unwrapEnvelope(e.blob)})),r=unifyColumns(t.map(e=>e.blob));if(0===r.length)return"";const s=[["cycle_index",...r].join(",")];for(const{cycleIndex:e,blob:o}of t){const t=r.reduce((e,t)=>Array.isArray(o[t])?Math.min(e,o[t].length):e,1/0),a=Number.isFinite(t)?t:0;for(let t=0;t<a;t++){const a=[escapeCsv(e)];for(const e of r){const r=o[e];a.push(escapeCsv(Array.isArray(r)?r[t]:""))}s.push(a.join(","))}}return s.join("\n")},rawBlobToCsv=e=>{const t=unwrapEnvelope(e);return cyclesToCsv([{cycleIndex:0,blob:t}]).split("\n").map(e=>{const t=e.indexOf(",");return t>=0?e.slice(t+1):""}).join("\n")},downloadCsv=(e,t)=>{const r=new Blob([t],{type:"text/csv;charset=utf-8;"}),s=URL.createObjectURL(r),o=document.createElement("a");o.href=s,o.download=e,document.body.appendChild(o),o.click(),o.remove(),URL.revokeObjectURL(s)};export const ResultHistoryTable=e=>{const t=useTis(),r=e.projectId??t.selection.projectId,s=e.methodId,[o,a]=useState([]),[n,i]=useState(!1),[l,d]=useState(null),{invoke:c}=useContext(EventEmitterContext),u=async()=>{if(r){i(!0);try{const e={project_id:r};s&&(e.method_id=s);const t=await c("tis.list_tests",MessageType.Request,e);t.success&&t.data&&t.data.tests&&a(t.data.tests)}catch(e){}i(!1)}else a([])};useEffect(()=>{u()},[r,s,t.state.activeRunId]);const m=async(e,t)=>{const o=e?.run_id,a=e?.method_id??s;if(!o||!a)return;const n="raw"===t?"tis.read_raw":"tis.read_filtered",i="raw"===t?"raw trace":"filtered trace",l="raw"===t?"raw":"filtered";d({runId:o,kind:t});try{let s="";if("raw"===t){const e=await c("tis.list_raw",MessageType.Request,{project_id:r,method_id:a,run_id:o}),t=(e?.data?.cycles??[]).filter(e=>"trace"===e?.name&&"number"==typeof e?.cycle_index).map(e=>e.cycle_index).sort((e,t)=>e-t);if(0===t.length){const e=await c(n,MessageType.Request,{project_id:r,method_id:a,run_id:o,name:"trace"});if(!e?.success||!e.data)return void alert(`No ${i} available for ${o}`+(e?.error_message?`: ${e.error_message}`:""));s=rawBlobToCsv(e.data)}else{const e=(await Promise.all(t.map(async e=>{const t=await c("tis.read_raw",MessageType.Request,{project_id:r,method_id:a,run_id:o,name:"trace",cycle_index:e});return t?.success?{cycleIndex:e,blob:t.data}:null}))).filter(e=>null!==e);if(0===e.length)return void alert(`No ${i} cycles readable for ${o}.`);s=cyclesToCsv(e)}}else{const e=await c(n,MessageType.Request,{project_id:r,method_id:a,run_id:o,name:"trace"});if(!e?.success||!e.data)return void alert(`No ${i} available for ${o}`+(e?.error_message?`: ${e.error_message}`:""));s=rawBlobToCsv(e.data)}if(!s)return void alert(`${i} for ${o} is empty or has no array columns.`);const d=f(p(e));downloadCsv(`${r}_${a}_${d?`${d}_`:""}${o}_${l}.csv`,s)}catch(e){alert(`Download failed: ${e instanceof Error?e.message:String(e)}`)}finally{d(null)}},p=e=>{const t="string"==typeof e?.sample_id?e.sample_id:"";if(t)return t;const r=e?.config;return r&&"object"==typeof r&&"string"==typeof r.sample_id?r.sample_id:""},f=e=>e.replace(/[\/\\:*?"<>|\0\x00-\x1f]/g,"_");return _jsxs("div",{style:{width:"100%",maxWidth:"100%",overflow:"hidden",boxSizing:"border-box"},children:[_jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"1rem"},children:[_jsx("h3",{style:{margin:0},children:r?`Test History: ${r}${s?` / ${s}`:""}`:"Test History (no project selected)"}),_jsx(Button,{icon:"pi pi-refresh",label:"Refresh",onClick:u,disabled:n})]}),_jsxs(DataTable,{value:o,loading:n,paginator:!0,rows:10,emptyMessage:"No tests found.",scrollable:!0,scrollHeight:"flex",tableStyle:{minWidth:0},style:{width:"100%"},selectionMode:"single",onSelectionChange:e=>{const r=e.value;r?.run_id&&t.setSelection({projectId:r.project_id??null,methodId:r.method_id??null,runId:r.run_id})},children:[_jsx(Column,{header:"Sample ID",sortable:!0,body:p,sortFunction:e=>{const t=[...e.data];return t.sort((t,r)=>p(t).localeCompare(p(r))*(e.order??1)),t},style:{minWidth:"8rem"}}),_jsx(Column,{field:"start_time",header:"Date/Time",sortable:!0,body:e=>{return(t=e.start_time)?new Date(t).toLocaleString():"";var t},style:{minWidth:"12rem"}}),_jsx(Column,{field:"method_id",header:"Test Method",sortable:!0,style:{minWidth:"10rem"}}),_jsx(Column,{field:"run_id",header:"Run ID",sortable:!0,style:{minWidth:"12rem"}}),_jsx(Column,{header:"Download",style:{width:"14rem"},body:e=>{const t=l?.runId===e.run_id&&"raw"===l?.kind,r=l?.runId===e.run_id&&"filtered"===l?.kind,s=null!==l;return _jsxs("div",{style:{display:"flex",gap:"0.4rem"},children:[_jsx(Button,{icon:t?"pi pi-spin pi-spinner":"pi pi-download",label:"Raw",size:"small",outlined:!0,disabled:s,onClick:()=>m(e,"raw"),tooltip:"Download raw_data/trace.json as CSV",tooltipOptions:{position:"left"}}),_jsx(Button,{icon:r?"pi pi-spin pi-spinner":"pi pi-download",label:"Filtered",size:"small",outlined:!0,disabled:s,onClick:()=>m(e,"filtered"),tooltip:"Download filtered_data/trace.json as CSV",tooltipOptions:{position:"left"}})]})}})]})]})};
@@ -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,CAkYpD,CAAC"}
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 +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,i=e.runId??t.selection.runId,s=e.schema??(a?t.schemas[a]:void 0),{throttleMs:l=100,cycleTableHeight:n="400px"}=e,{invoke:o,subscribe:c,unsubscribe:d}=useContext(EventEmitterContext),[m,u]=useState(null),[p,h]=useState([]),[f,g]=useState({}),[y,_]=useState(!1),[x,b]=useState(!1),[j,v]=useState(null),[w,S]=useState(null),[C,T]=useState(!1),[R,N]=useState(null),[D,I]=useState(null),[L,A]=useState(!1),O=useMemo(()=>{const e=[];for(const[t,r]of Object.entries(s?.views??{}))"cycle_scatter"===r.type&&e.push({name:t,view:r});return e},[s]),[E,M]=useState(O.length>0?O[0].name:null);useEffect(()=>{if(0===O.length)return;null!==E&&O.some(e=>e.name===E)||M(O[0].name)},[O,E]);const k=useRef([]),z=useRef(null),F=useRef(null),H=()=>{F.current||(F.current=setTimeout(()=>{if(F.current=null,k.current.length>0){const e=k.current;k.current=[],h(t=>[...e.slice().reverse(),...t])}z.current&&(g(z.current),z.current=null)},l))};useEffect(()=>{if(!r||!a||!i)return u(null),h([]),void g({});let e=!1;return(async()=>{try{const t=await o("tis.read_test",MessageType.Request,{project_id:r,method_id:a,run_id:i});!e&&t?.success&&(u(t.data),g(t.data.results??{}));const s=await o("tis.read_cycles",MessageType.Request,{project_id:r,method_id:a,run_id:i,offset:0,limit:200,order:"desc"});!e&&s?.success&&h(s.data.cycles??[])}catch(e){}})(),()=>{e=!0}},[r,a,i,o]),useEffect(()=>{const e=e=>e?.project_id===r&&e?.method_id===a&&e?.run_id===i,t=c("tis.cycle_added",t=>{e(t)&&t.cycle&&(k.current.push(t.cycle),H())}),s=c("tis.results_updated",t=>{e(t)&&(z.current=t.results??{},H())});return()=>{d(t),d(s),F.current&&(clearTimeout(F.current),F.current=null)}},[r,a,i,l]);const P=useMemo(()=>{if(!E||0===O.length)return null;const e=O.find(e=>e.name===E)?.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,E,O]),V=O.find(e=>e.name===E)?.view,$=V?.y.some(e=>"right"===e.y_axis)??!1,B=useMemo(()=>({responsive:!0,maintainAspectRatio:!1,scales:{x:{title:{display:!!V?.x.label,text:V?.x.label}},y:{position:"left",title:{display:!0,text:leftAxisLabel(V)}},...$?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:rightAxisLabel(V)}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},mode:"xy"}}}}),[V,$]),q=s?.raw_data?.blob_name??"trace",J=useRef(""),G=useCallback(async()=>{if(!r||!a||!i)return;const e=`${r}|${a}|${i}|${q}`;if(J.current!==e){J.current=e,T(!0),S(null),v(null);try{const e=await o("tis.read_raw",MessageType.Request,{project_id:r,method_id:a,run_id:i,name:q});e?.success?v(e.data??{}):S(e?.error_message??"No raw data on disk for this run.")}catch(e){S(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:i,name:q});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,i,q,o]);useEffect(()=>{J.current=""},[r,a,i,q]);return r&&a&&i&&s?_jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsx(Header,{meta:m,config:m?.config,runId:i,projectId:r,methodId:a,canViewRaw:!!s.raw_data,onViewRaw:()=>{_(!0),G()},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:E,options:O.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>M(e.value),placeholder:0===O.length?"No view defined":"Select a view",disabled:0===O.length}),_jsx("h3",{style:{margin:0},children:V?.title??""})]}),_jsx("div",{style:{height:320},children:P&&_jsx(Line,{data:P,options:B})})]}),_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: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&&_jsx(Dialog,{visible:y,onHide:()=>_(!1),header:`Run Data — ${i}`,style:{width:"90vw",height:"80vh"},maximizable:!0,children:_jsxs(TabView,{style:{height:"100%"},children:[_jsx(TabPanel,{header:"Raw Data",children:_jsx(DataBlobTable,{blob:j,loading:C,error:w,rawData:s.raw_data})}),_jsx(TabPanel,{header:"Filtered Data",children:_jsx(DataBlobTable,{blob:R,loading:L,error:D,rawData:s.raw_data,emptyMessage:"Filtered data is written by post-processing — none on disk for this run yet."})})]})}),_jsx(Dialog,{visible:x,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:i,canViewRaw:s,onViewRaw:l,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: ",i," · 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:l,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))})},DataBlobTable=({blob:e,loading:t,error:r,rawData:a,emptyMessage:i})=>{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:i??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??{}),l=Object.keys(e).filter(t=>Array.isArray(e[t])),n=[];for(const t of s)Array.isArray(e[t])&&n.push(t);for(const e of l)n.includes(e)||n.push(e);if(0===n.length)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:i??"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:i??"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";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 +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,CAuJ1D,CAAC"}
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 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs}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(),a=e.projectId??t.selection.projectId,s=e.methodId??t.selection.methodId,o=e.runId??t.selection.runId,i=e.schema??(s?t.schemas[s]:void 0),{blobName:r,chartHeight:n="60vh"}=e,{invoke:l}=useContext(EventEmitterContext),[m,d]=useState(null),[c,p]=useState(!0),[u,h]=useState(null),x=useRef(null),y=useMemo(()=>{const e=[];for(const[t,a]of Object.entries(i?.views??{}))"raw_trace"===a.type&&e.push({name:t,view:a});return e},[i]),[f,g]=useState(y.length>0?y[0].name:null),_=r??i?.raw_data?.blob_name??"trace";useEffect(()=>{if(!a||!s||!o)return d(null),p(!1),void h(null);let e=!1;return p(!0),h(null),(async()=>{try{const t=await l("tis.read_raw",MessageType.Request,{project_id:a,method_id:s,run_id:o,name:_});if(e)return;t?.success?d(t.data??{}):h(t?.error_message??"Failed to read raw data")}catch(t){e||h(String(t?.message??t))}finally{e||p(!1)}})(),()=>{e=!0}},[a,s,o,_,l]);const v=useMemo(()=>{if(!m||!f)return null;const e=y.find(e=>e.name===f)?.view;if(!e)return null;const t=e.x.column,a=m[t]??[];return{datasets:e.y.map((e,t)=>({label:e.label??e.column,data:(m[e.column]??[]).map((e,t)=>({x:a[t],y:e})),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(t),backgroundColor:palette(t),pointRadius:0,borderWidth:1.5,showLine:!0}))}},[m,f,y]),j=y.find(e=>e.name===f)?.view,b=j?.y.some(e=>"right"===e.y_axis)??!1,w=useMemo(()=>({responsive:!0,maintainAspectRatio:!1,parsing:!1,scales:{x:{type:"linear",title:{display:!!j?.x.label,text:j?.x.label}},y:{position:"left",title:{display:!0,text:axisLabel(j,"left")}},...b?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:axisLabel(j,"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"}}}}),[j,b]);return a&&s&&o?i?i.raw_data?0===y.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"},children:[_jsx(Dropdown,{value:f,options:y.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>g(e.value),placeholder:"Select a view"}),_jsx("h3",{style:{margin:0},children:j?.title??""}),_jsx("div",{style:{flex:1}}),_jsx(Button,{icon:"pi pi-refresh",label:"Reset Zoom",outlined:!0,onClick:()=>x.current?.resetZoom?.()})]}),_jsxs("div",{style:{flex:1,minHeight:0,height:n,position:"relative"},children:[c&&_jsx(Overlay,{children:"Loading raw data…"}),u&&_jsx(Overlay,{children:u}),v&&!c&&!u&&_jsx(Line,{ref:x,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}),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,{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(" / ")??"";
@@ -24,6 +24,13 @@ export interface TestFieldDef {
24
24
  label?: string;
25
25
  /** Long-form guidance surfaced as a hover tooltip on an info icon. */
26
26
  description?: string;
27
+ /** Seed value applied when the operator selects this test method.
28
+ * For source-bound fields the default is written to the GM tag so
29
+ * the control program sees it; for non-source fields it's stashed
30
+ * straight into stagedConfig. Operator edits override per-stage,
31
+ * but the schema default never mutates — re-selecting the method
32
+ * re-applies the default. */
33
+ default?: any;
27
34
  }
28
35
  export interface TestMethod {
29
36
  project_fields: TestFieldDef[];
@@ -1 +1 @@
1
- {"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAcxE;;;;;GAKG;AACH,UAAU,WAAW;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,GAAG,aAAa,CAAC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IACzB,iEAAiE;IACjE,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;IAChB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACvB,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B;0CACsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;2CACuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;gEAG4D;IAC5D,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf;;sDAEkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAChE;AAyED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAobtD,CAAC"}
1
+ {"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2D,MAAM,OAAO,CAAC;AAchF;;;;;GAKG;AACH,UAAU,WAAW;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,GAAG,aAAa,CAAC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IACzB,iEAAiE;IACjE,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;IAChB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;kCAK8B;IAC9B,OAAO,CAAC,EAAE,GAAG,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACvB,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B;0CACsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;2CACuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;gEAG4D;IAC5D,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf;;sDAEkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAChE;AAyED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAsdtD,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useMemo}from"react";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Dropdown}from"primereact/dropdown";import{Dialog}from"primereact/dialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{MessageType}from"../../hub/CommandMessage";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";import{useTis}from"./TisProvider";import{useAmsAssets,useAmsRoles}from"../ams/AmsProvider";import{TestMethodDialog}from"./TestMethodDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},hasDescription=e=>"string"==typeof e.description&&e.description.length>0,methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e,AssetIdPicker=({assetType:e,value:t,onChange:s,invalid:i})=>{const a=useAmsAssets(),n=useAmsRoles(),o=useMemo(()=>a.filter(t=>t.asset_type===e&&"active"===t.status).map(e=>{const t=n[e.asset_type]?.find(t=>t.location===e.location)?.label,s=[e.asset_id];return t?s.push(`— ${t}`):e.location&&s.push(`— ${e.location}`),e.serial&&s.push(`(s/n ${e.serial})`),{label:s.join(" "),value:e.asset_id}}),[a,n,e]);return _jsx(Dropdown,{value:t,options:o,onChange:e=>s(e.value??""),placeholder:0===o.length?`No active ${e} assets registered — add one in Settings → Assets`:`Select ${e}…`,className:i?"p-invalid":"",filter:!0,showClear:!0,disabled:0===o.length})};export const TestSetupForm=({schema:e,defaultMethodId:t,onMethodChange:s,onValidationChange:i})=>{const a=useTis(),{invoke:n,write:o}=useContext(EventEmitterContext),{rawValues:r,findTagByFqdn:l}=useContext(AutoCoreTagContext),d=useMemo(()=>Object.keys(a.schemas),[a.schemas]),c=a.selection.projectId,m=""!==c.trim()&&a.projectKnown(c.trim()),[p,u]=useState(a.selection.methodId||t||a.defaultMethodId||""),[h,f]=useState(a.selection.sampleId||""),g=a.stagedConfig,x=e=>{const t="function"==typeof e?e(a.stagedConfig):e;t!==a.stagedConfig&&a.setStagedConfig(t)},_=e??(p?a.schemas[p]:void 0);useEffect(()=>{a.selection.methodId!==p&&p&&a.setSelection({methodId:p}),s&&s(p)},[p]),useEffect(()=>{a.selection.sampleId!==h&&a.setSelection({sampleId:h})},[h]),useEffect(()=>{a.state.stagedSampleId&&a.state.stagedSampleId!==h&&f(a.state.stagedSampleId)},[a.state.stagedSampleId]),useEffect(()=>{a.selection.methodId&&a.selection.methodId!==p&&u(a.selection.methodId)},[a.selection.methodId]);const[j,y]=useState(!1),[v,b]=useState(!1),[I,C]=useState({open:!1,title:"",body:null});useEffect(()=>{_&&x(e=>{let t=e;for(const s of _.config_fields){if("sample_id"===s.name)continue;if(!s.source)continue;const i=l(s.source);if(!i)continue;const a=r[i.tagName];null!=a&&(t[s.name]!==a&&(t===e&&(t={...e}),t[s.name]=a))}return t})},[_,r,l]),useEffect(()=>{if(!_)return void y(!1);let e=!0;m||(e=!1),p.trim()||(e=!1),h.trim()||(e=!1),e&&!a.projectFieldsLoaded&&(e=!1);for(const t of _.config_fields)if("sample_id"!==t.name&&t.required){const s=g[t.name];if(void 0===s||""===s||null===s){e=!1;break}}if(y(e),i&&i(e,g),e){const{sample_id:e,...t}=g??{},s={...a.projectFields,...t};n("tis.stage_test",MessageType.Request,{project_id:c,method_id:p,sample_id:h,config:s}).catch(e=>{})}},[g,_,c,p,h,m,a.projectFields,a.projectFieldsLoaded,i,n]);const S=async(e,t)=>{if(x({...g,[e.name]:t}),e.source)try{await o(e.source,t)}catch(e){}};if(!_)return _jsx("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:_jsx("h3",{className:"ac-form-section",children:a.schemasLoaded?"No Test Method Selected":"Loading test methods…"})});if(!m)return _jsxs("div",{style:{padding:"1.25rem",maxWidth:"600px"},children:[_jsx("h3",{className:"ac-form-section",children:"No project selected"}),_jsxs("p",{style:{color:"var(--text-secondary-color)",marginTop:"0.5rem"},children:["Pick a project on the ",_jsx("strong",{children:"Project"})," tab first",""!==c.trim()&&` (or click + there to create "${c.trim()}")`,"."]})]});const T=j&&!a.state.lastStartError;return _jsxs("div",{className:"ac-form-grid",style:{padding:"1.25rem",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Test Setup",_jsx(Button,{icon:T?"pi pi-check-circle":"pi pi-exclamation-circle",severity:T?"success":"danger",text:!0,rounded:!0,"aria-label":"Test Setup status",onClick:()=>{const e=(()=>{const e=[];if(m||e.push("No project is selected. Pick or create one on the Project tab."),a.projectFieldsLoaded||e.push("Project fields are still loading."),p.trim()||e.push("No test method selected."),h.trim()||e.push("Sample ID is required."),_)for(const t of _.config_fields){if("sample_id"===t.name)continue;if(!t.required)continue;const s=g[t.name];void 0!==s&&""!==s&&null!==s||e.push(`Required field "${labelOf(t)}" is empty.`)}return e})(),t=a.state.lastStartError?.trim()??"";let s;s=0!==e.length||t?_jsxs("div",{children:[e.length>0&&_jsxs(_Fragment,{children:[_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:"The form is incomplete:"}),_jsx("ul",{style:{margin:"0 0 1rem 1.25rem"},children:e.map((e,t)=>_jsx("li",{children:e},t))})]}),t&&_jsxs(_Fragment,{children:[_jsx("p",{style:{margin:"0 0 0.25rem 0",fontWeight:600},children:"Last start_test error from the server:"}),_jsx("pre",{style:{margin:0,padding:"0.5rem",background:"rgba(0,0,0,0.25)",borderRadius:"4px",whiteSpace:"pre-wrap",fontSize:"0.875rem"},children:t})]})]}):_jsx("p",{style:{margin:0},children:"All required fields are complete. The test is staged and ready to start."}),C({open:!0,title:"Test Setup Status",body:s})}}),_jsxs("span",{style:{fontSize:"0.85em",color:"var(--text-secondary-color)",fontWeight:"normal",marginLeft:"0.25rem"},children:["project: ",_jsx("strong",{children:c})]})]}),_jsx("span",{className:"ac-form-label",children:"Sample ID"}),_jsx(TextInput,{label:void 0,value:h,onValueChanged:e=>{f(e)},className:h.trim()?"":"p-invalid"}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:h.trim()?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:h.trim()?"pi pi-check":"pi pi-times"})}),d.length>0&&_jsxs(_Fragment,{children:[_jsx("span",{className:"ac-form-label",children:"Test Method"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(InputText,{value:methodLabelOf(p,_),readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-pencil",type:"button",onClick:()=>b(!0),tooltip:d.length>1?"Change test method":"View test method details",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:p?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:p?"pi pi-check":"pi pi-times"})})]}),_jsx("h3",{className:"ac-form-section",style:{marginTop:"1rem"},children:"Test Configuration"}),_.config_fields.map(e=>{if("sample_id"===e.name)return null;const t=(e=>{if(!e.required)return!0;const t=g[e.name];return void 0!==t&&""!==t&&null!==t})(e),s=(e=>{const t=_?.asset_refs??[],s=`config.${e.name}`,i=t.find(e=>"by_id_field"===e.select&&e.from===s);return i?i.asset_type:null})(e),i=!s&&"string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),s?_jsx(AssetIdPicker,{assetType:s,value:null!=g[e.name]?String(g[e.name]):"",onChange:t=>S(e,t),invalid:!t}):i?_jsx(ValueInput,{label:void 0,value:null!=g[e.name]?Number(g[e.name]):null,onValueChanged:t=>S(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=g[e.name]?String(g[e.name]):"",onValueChanged:t=>S(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsx(Button,{icon:"pi pi-info-circle",text:!0,rounded:!0,"aria-label":`About ${labelOf(e)}`,onClick:()=>C({open:!0,title:labelOf(e),body:_jsx("p",{style:{margin:0,whiteSpace:"pre-wrap"},children:e.description})})}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:t?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:t?"pi pi-check":"pi pi-times"})})]},e.name)}),_jsx(TestMethodDialog,{visible:v,onHide:()=>b(!1),currentMethodId:p,onSelected:e=>u(e)}),_jsx(Dialog,{header:I.title,visible:I.open,onHide:()=>C({open:!1,title:"",body:null}),style:{width:"32rem",maxWidth:"90vw"},modal:!0,dismissableMask:!0,children:I.body})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useMemo,useRef}from"react";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Dropdown}from"primereact/dropdown";import{Dialog}from"primereact/dialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{MessageType}from"../../hub/CommandMessage";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";import{useTis}from"./TisProvider";import{useAmsAssets,useAmsRoles}from"../ams/AmsProvider";import{TestMethodDialog}from"./TestMethodDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},hasDescription=e=>"string"==typeof e.description&&e.description.length>0,methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e,AssetIdPicker=({assetType:e,value:t,onChange:s,invalid:i})=>{const a=useAmsAssets(),n=useAmsRoles(),o=useMemo(()=>a.filter(t=>t.asset_type===e&&"active"===t.status).map(e=>{const t=n[e.asset_type]?.find(t=>t.location===e.location)?.label,s=[e.asset_id];return t?s.push(`— ${t}`):e.location&&s.push(`— ${e.location}`),e.serial&&s.push(`(s/n ${e.serial})`),{label:s.join(" "),value:e.asset_id}}),[a,n,e]);return _jsx(Dropdown,{value:t,options:o,onChange:e=>s(e.value??""),placeholder:0===o.length?`No active ${e} assets registered — add one in Settings → Assets`:`Select ${e}…`,className:i?"p-invalid":"",filter:!0,showClear:!0,disabled:0===o.length})};export const TestSetupForm=({schema:e,defaultMethodId:t,onMethodChange:s,onValidationChange:i})=>{const a=useTis(),{invoke:n,write:o}=useContext(EventEmitterContext),{rawValues:r,findTagByFqdn:l}=useContext(AutoCoreTagContext),c=useMemo(()=>Object.keys(a.schemas),[a.schemas]),d=a.selection.projectId,m=""!==d.trim()&&a.projectKnown(d.trim()),[p,u]=useState(a.selection.methodId||t||a.defaultMethodId||""),[f,h]=useState(a.selection.sampleId||""),g=a.stagedConfig,x=e=>{const t="function"==typeof e?e(a.stagedConfig):e;t!==a.stagedConfig&&a.setStagedConfig(t)},_=e??(p?a.schemas[p]:void 0);useEffect(()=>{a.selection.methodId!==p&&p&&a.setSelection({methodId:p}),s&&s(p)},[p]),useEffect(()=>{a.selection.sampleId!==f&&a.setSelection({sampleId:f})},[f]),useEffect(()=>{a.state.stagedSampleId&&a.state.stagedSampleId!==f&&h(a.state.stagedSampleId)},[a.state.stagedSampleId]),useEffect(()=>{a.selection.methodId&&a.selection.methodId!==p&&u(a.selection.methodId)},[a.selection.methodId]);const[j,v]=useState(!1),[y,b]=useState(!1),[I,C]=useState({open:!1,title:"",body:null}),S=useRef("");useEffect(()=>{_&&p&&S.current!==p&&(S.current=p,x(e=>{let t=e;for(const s of _.config_fields)"sample_id"!==s.name&&void 0!==s.default&&null!==s.default&&(t===e&&(t={...e}),t[s.name]=s.default,s.source&&Promise.resolve().then(()=>o(s.source,s.default)).catch(e=>{}));return t}))},[_,p,o]),useEffect(()=>{_&&x(e=>{let t=e;for(const s of _.config_fields){if("sample_id"===s.name)continue;if(!s.source)continue;const i=l(s.source);if(!i)continue;const a=r[i.tagName];null!=a&&(t[s.name]!==a&&(t===e&&(t={...e}),t[s.name]=a))}return t})},[_,r,l]),useEffect(()=>{if(!_)return void v(!1);let e=!0;m||(e=!1),p.trim()||(e=!1),f.trim()||(e=!1),e&&!a.projectFieldsLoaded&&(e=!1);for(const t of _.config_fields)if("sample_id"!==t.name&&t.required){const s=g[t.name];if(void 0===s||""===s||null===s){e=!1;break}}if(v(e),i&&i(e,g),e){const{sample_id:e,...t}=g??{},s={...a.projectFields,...t};n("tis.stage_test",MessageType.Request,{project_id:d,method_id:p,sample_id:f,config:s}).catch(e=>{})}},[g,_,d,p,f,m,a.projectFields,a.projectFieldsLoaded,i,n]);const T=async(e,t)=>{if(x({...g,[e.name]:t}),e.source)try{await o(e.source,t)}catch(e){}};if(!_)return _jsx("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:_jsx("h3",{className:"ac-form-section",children:a.schemasLoaded?"No Test Method Selected":"Loading test methods…"})});if(!m)return _jsxs("div",{style:{padding:"1.25rem",maxWidth:"600px"},children:[_jsx("h3",{className:"ac-form-section",children:"No project selected"}),_jsxs("p",{style:{color:"var(--text-secondary-color)",marginTop:"0.5rem"},children:["Pick a project on the ",_jsx("strong",{children:"Project"})," tab first",""!==d.trim()&&` (or click + there to create "${d.trim()}")`,"."]})]});const N=j&&!a.state.lastStartError;return _jsxs("div",{className:"ac-form-grid",style:{padding:"1.25rem",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Test Setup",_jsx(Button,{icon:N?"pi pi-check-circle":"pi pi-exclamation-circle",severity:N?"success":"danger",text:!0,rounded:!0,"aria-label":"Test Setup status",onClick:()=>{const e=(()=>{const e=[];if(m||e.push("No project is selected. Pick or create one on the Project tab."),a.projectFieldsLoaded||e.push("Project fields are still loading."),p.trim()||e.push("No test method selected."),f.trim()||e.push("Sample ID is required."),_)for(const t of _.config_fields){if("sample_id"===t.name)continue;if(!t.required)continue;const s=g[t.name];void 0!==s&&""!==s&&null!==s||e.push(`Required field "${labelOf(t)}" is empty.`)}return e})(),t=a.state.lastStartError?.trim()??"";let s;s=0!==e.length||t?_jsxs("div",{children:[e.length>0&&_jsxs(_Fragment,{children:[_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:"The form is incomplete:"}),_jsx("ul",{style:{margin:"0 0 1rem 1.25rem"},children:e.map((e,t)=>_jsx("li",{children:e},t))})]}),t&&_jsxs(_Fragment,{children:[_jsx("p",{style:{margin:"0 0 0.25rem 0",fontWeight:600},children:"Last start_test error from the server:"}),_jsx("pre",{style:{margin:0,padding:"0.5rem",background:"rgba(0,0,0,0.25)",borderRadius:"4px",whiteSpace:"pre-wrap",fontSize:"0.875rem"},children:t})]})]}):_jsx("p",{style:{margin:0},children:"All required fields are complete. The test is staged and ready to start."}),C({open:!0,title:"Test Setup Status",body:s})}}),_jsxs("span",{style:{fontSize:"0.85em",color:"var(--text-secondary-color)",fontWeight:"normal",marginLeft:"0.25rem"},children:["project: ",_jsx("strong",{children:d})]})]}),_jsx("span",{className:"ac-form-label",children:"Sample ID"}),_jsx(TextInput,{label:void 0,value:f,onValueChanged:e=>{h(e)},className:f.trim()?"":"p-invalid"}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:f.trim()?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:f.trim()?"pi pi-check":"pi pi-times"})}),c.length>0&&_jsxs(_Fragment,{children:[_jsx("span",{className:"ac-form-label",children:"Test Method"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(InputText,{value:methodLabelOf(p,_),readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-pencil",type:"button",onClick:()=>b(!0),tooltip:c.length>1?"Change test method":"View test method details",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:p?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:p?"pi pi-check":"pi pi-times"})})]}),_jsx("h3",{className:"ac-form-section",style:{marginTop:"1rem"},children:"Test Configuration"}),_.config_fields.map(e=>{if("sample_id"===e.name)return null;const t=(e=>{if(!e.required)return!0;const t=g[e.name];return void 0!==t&&""!==t&&null!==t})(e),s=(e=>{const t=_?.asset_refs??[],s=`config.${e.name}`,i=t.find(e=>"by_id_field"===e.select&&e.from===s);return i?i.asset_type:null})(e),i=!s&&"string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),s?_jsx(AssetIdPicker,{assetType:s,value:null!=g[e.name]?String(g[e.name]):"",onChange:t=>T(e,t),invalid:!t}):i?_jsx(ValueInput,{label:void 0,value:null!=g[e.name]?Number(g[e.name]):null,onValueChanged:t=>T(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=g[e.name]?String(g[e.name]):"",onValueChanged:t=>T(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsx(Button,{icon:"pi pi-info-circle",text:!0,rounded:!0,"aria-label":`About ${labelOf(e)}`,onClick:()=>C({open:!0,title:labelOf(e),body:_jsx("p",{style:{margin:0,whiteSpace:"pre-wrap"},children:e.description})})}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:t?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:t?"pi pi-check":"pi pi-times"})})]},e.name)}),_jsx(TestMethodDialog,{visible:y,onHide:()=>b(!1),currentMethodId:p,onSelected:e=>u(e)}),_jsx(Dialog,{header:I.title,visible:I.open,onHide:()=>C({open:!1,title:"",body:null}),style:{width:"32rem",maxWidth:"90vw"},modal:!0,dismissableMask:!0,children:I.body})]})};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.73",
3
+ "version": "3.3.75",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -91,6 +91,10 @@
91
91
  "import": "./dist/components/*",
92
92
  "types": "./dist/components/*.d.ts"
93
93
  },
94
+ "./assets/*": {
95
+ "import": "./dist/assets/*",
96
+ "types": "./dist/assets/*.d.ts"
97
+ },
94
98
  "./hooks/*": {
95
99
  "import": "./dist/hooks/*",
96
100
  "types": "./dist/hooks/*.d.ts"
@@ -0,0 +1,37 @@
1
+ import type { SVGProps } from "react";
2
+
3
+ export const HomeMotor = (props: SVGProps<SVGSVGElement>) => (
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ {...props}
15
+ >
16
+ {/* The House Outline */}
17
+ <path d="M8.5 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-2" />
18
+
19
+ {/* The Motor Outer Casing */}
20
+ <circle
21
+ cx="12"
22
+ cy="12.5"
23
+ r="4"
24
+ />
25
+
26
+ {/* The Motor Shaft / Center Point */}
27
+ <circle
28
+ cx="12"
29
+ cy="12.5"
30
+ r="1"
31
+ fill="currentColor"
32
+ stroke="none"
33
+ />
34
+ </svg>
35
+ );
36
+
37
+ export default HomeMotor;
@@ -0,0 +1,57 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ class="lucide lucide-house-plug-icon lucide-house-plug"
12
+ version="1.1"
13
+ id="svg4"
14
+ sodipodi:docname="home-motor.svg"
15
+ inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
16
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
17
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ xmlns:svg="http://www.w3.org/2000/svg">
20
+ <defs
21
+ id="defs4" />
22
+ <sodipodi:namedview
23
+ id="namedview4"
24
+ pagecolor="#ffffff"
25
+ bordercolor="#000000"
26
+ borderopacity="0.25"
27
+ inkscape:showpageshadow="2"
28
+ inkscape:pageopacity="0.0"
29
+ inkscape:pagecheckerboard="0"
30
+ inkscape:deskcolor="#d1d1d1"
31
+ inkscape:zoom="50.541667"
32
+ inkscape:cx="12"
33
+ inkscape:cy="12"
34
+ inkscape:window-width="2560"
35
+ inkscape:window-height="1494"
36
+ inkscape:window-x="-11"
37
+ inkscape:window-y="-11"
38
+ inkscape:window-maximized="1"
39
+ inkscape:current-layer="svg4" />
40
+ <path
41
+ d="M8.5 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-2"
42
+ id="path4" />
43
+ <ellipse
44
+ style="fill:none;stroke-width:2.08003;stroke-dasharray:none"
45
+ id="path5"
46
+ cx="11.970321"
47
+ cy="12.366035"
48
+ rx="3.9292588"
49
+ ry="3.9292586" />
50
+ <ellipse
51
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:4.98654;stroke-dasharray:none;stroke-opacity:1"
52
+ id="path6"
53
+ cx="11.970321"
54
+ cy="12.366035"
55
+ rx="0.89035445"
56
+ ry="0.89035451" />
57
+ </svg>
@@ -75,7 +75,7 @@ interface ValueInputProps extends Omit<InputNumberProps, 'value' | 'size'> {
75
75
  /**
76
76
  * The label for the ValueInput field.
77
77
  */
78
- label: React.ReactNode | undefined;
78
+ label: React.ReactNode | undefined | null;
79
79
 
80
80
 
81
81
  /**
@@ -205,7 +205,7 @@ interface ValueInputProps extends Omit<InputNumberProps, 'value' | 'size'> {
205
205
  * accepting and rejecting values and keyboard management.
206
206
  */
207
207
  export const ValueInput: React.FC<ValueInputProps> = ({
208
- label,
208
+ label = undefined,
209
209
  value = null,
210
210
  min = undefined,
211
211
  max = undefined,
@@ -24,40 +24,98 @@ export interface ResultHistoryTableProps {
24
24
  }
25
25
 
26
26
  /**
27
- * Convert a raw_data blob (`{ colA: [...], colB: [...], ... }`) into a CSV
28
- * string. Keys with scalar (non-array) values are skipped; array lengths are
29
- * truncated to the shortest column so the grid is rectangular.
30
- *
31
- * Column order: `t` first if present (it's the canonical x-axis in every
32
- * current test schema), then the remaining keys in their JSON order. Each
33
- * cell is quoted only when it contains a comma, quote, or newline — matching
34
- * RFC 4180.
27
+ * Peel a tis.read_raw response. The wire shape may be either a per-cycle
28
+ * envelope `{ cycle_index, cycle_fields, context, data }` or the legacy
29
+ * flat `{ col: number[] }` blob. CSV builders only want the columnar
30
+ * `{ col: number[] }` part.
35
31
  */
36
- const rawBlobToCsv = (blob: any): string => {
37
- if (!blob || typeof blob !== 'object') return '';
38
-
39
- const entries: Array<[string, any[]]> = Object.entries(blob)
40
- .filter(([, v]) => Array.isArray(v)) as Array<[string, any[]]>;
41
- if (entries.length === 0) return '';
32
+ const unwrapEnvelope = (blob: any): Record<string, any[]> => {
33
+ if (!blob || typeof blob !== 'object') return {};
34
+ if ('data' in blob && blob.data && typeof blob.data === 'object'
35
+ && Object.values(blob.data).some(v => Array.isArray(v))) {
36
+ return blob.data as Record<string, any[]>;
37
+ }
38
+ return blob as Record<string, any[]>;
39
+ };
42
40
 
43
- entries.sort(([a], [b]) => (a === 't' ? -1 : b === 't' ? 1 : 0));
41
+ /**
42
+ * RFC 4180 escape: quote only when the value contains comma, quote, or newline.
43
+ */
44
+ const escapeCsv = (v: any): string => {
45
+ if (v === null || v === undefined) return '';
46
+ const s = String(v);
47
+ return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
48
+ };
44
49
 
45
- const columns = entries.map(([k]) => k);
46
- const nRows = entries.reduce((min, [, arr]) => Math.min(min, arr.length), Infinity);
50
+ /**
51
+ * Build the canonical column order for a set of cycle blobs: `t` first
52
+ * (canonical x-axis in every current schema), then the union of remaining
53
+ * array-valued keys in first-seen order across cycles.
54
+ */
55
+ const unifyColumns = (blobs: Record<string, any[]>[]): string[] => {
56
+ const seen = new Set<string>();
57
+ const out: string[] = [];
58
+ for (const blob of blobs) {
59
+ for (const [k, v] of Object.entries(blob)) {
60
+ if (Array.isArray(v) && !seen.has(k)) {
61
+ seen.add(k);
62
+ out.push(k);
63
+ }
64
+ }
65
+ }
66
+ out.sort((a, b) => (a === 't' ? -1 : b === 't' ? 1 : 0));
67
+ return out;
68
+ };
47
69
 
48
- const escape = (v: any): string => {
49
- if (v === null || v === undefined) return '';
50
- const s = String(v);
51
- return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
52
- };
70
+ /**
71
+ * Concatenate one or more cycle envelopes into a single CSV. Adds a leading
72
+ * `cycle_index` column so downstream consumers can split groups apart again.
73
+ * Missing columns in a given cycle become empty cells.
74
+ */
75
+ const cyclesToCsv = (cycles: Array<{ cycleIndex: number; blob: any }>): string => {
76
+ if (cycles.length === 0) return '';
77
+ const unwrapped = cycles.map(c => ({
78
+ cycleIndex: c.cycleIndex,
79
+ blob: unwrapEnvelope(c.blob),
80
+ }));
81
+ const columns = unifyColumns(unwrapped.map(u => u.blob));
82
+ if (columns.length === 0) return '';
53
83
 
54
- const lines: string[] = [columns.join(',')];
55
- for (let i = 0; i < nRows; i++) {
56
- lines.push(entries.map(([, arr]) => escape(arr[i])).join(','));
84
+ const lines: string[] = [['cycle_index', ...columns].join(',')];
85
+ for (const { cycleIndex, blob } of unwrapped) {
86
+ const nRows = columns.reduce(
87
+ (min, c) => Array.isArray(blob[c]) ? Math.min(min, blob[c].length) : min,
88
+ Infinity,
89
+ );
90
+ const finite = Number.isFinite(nRows) ? (nRows as number) : 0;
91
+ for (let i = 0; i < finite; i++) {
92
+ const row = [escapeCsv(cycleIndex)];
93
+ for (const c of columns) {
94
+ const arr = blob[c];
95
+ row.push(escapeCsv(Array.isArray(arr) ? arr[i] : ''));
96
+ }
97
+ lines.push(row.join(','));
98
+ }
57
99
  }
58
100
  return lines.join('\n');
59
101
  };
60
102
 
103
+ /**
104
+ * Single-blob CSV — kept for the filtered-data download path, which is
105
+ * not (yet) per-cycle on the server.
106
+ */
107
+ const rawBlobToCsv = (blob: any): string => {
108
+ const unwrapped = unwrapEnvelope(blob);
109
+ return cyclesToCsv([{ cycleIndex: 0, blob: unwrapped }])
110
+ // Strip the synthetic cycle_index column for the legacy single-blob path.
111
+ .split('\n')
112
+ .map(line => {
113
+ const idx = line.indexOf(',');
114
+ return idx >= 0 ? line.slice(idx + 1) : '';
115
+ })
116
+ .join('\n');
117
+ };
118
+
61
119
  /**
62
120
  * Browser download shim: turn a string into a transient blob URL, click it,
63
121
  * and clean up. Works without any extra libraries.
@@ -126,37 +184,86 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
126
184
  const rowMethodId = rowData?.method_id ?? methodId;
127
185
  if (!runId || !rowMethodId) return;
128
186
 
129
- // raw_data/ and filtered_data/ are symmetric dirs on disk; the same
130
- // blob name ("trace") exists in both. Only the IPC topic and filename
131
- // suffix change.
187
+ // raw_data/ is per-cycle on disk; filtered_data/ is still a single
188
+ // file per test. The raw path lists cycles via tis.list_raw and
189
+ // concatenates them into one CSV with a leading cycle_index
190
+ // column. Filtered keeps its one-shot tis.read_filtered.
132
191
  const topic = kind === 'raw' ? 'tis.read_raw' : 'tis.read_filtered';
133
192
  const label = kind === 'raw' ? 'raw trace' : 'filtered trace';
134
193
  const suffix = kind === 'raw' ? 'raw' : 'filtered';
135
194
 
136
195
  setDownloading({ runId, kind });
137
196
  try {
138
- const resp: any = await invoke(topic as any, MessageType.Request, {
139
- project_id: projectId,
140
- method_id: rowMethodId,
141
- run_id: runId,
142
- name: 'trace',
143
- } as any);
197
+ let csv = '';
144
198
 
145
- if (!resp?.success || !resp.data) {
146
- console.warn(`${topic} returned no data for`, runId, resp?.error_message);
147
- alert(
148
- `No ${label} available for ${runId}` +
149
- (resp?.error_message ? `: ${resp.error_message}` : '')
199
+ if (kind === 'raw') {
200
+ // List every cycle on disk for this run/blob.
201
+ const listResp: any = await invoke(
202
+ 'tis.list_raw' as any, MessageType.Request, {
203
+ project_id: projectId, method_id: rowMethodId, run_id: runId,
204
+ } as any,
150
205
  );
151
- return;
206
+ const cycleEntries: any[] = listResp?.data?.cycles ?? [];
207
+ const cycleIdxs = cycleEntries
208
+ .filter(c => c?.name === 'trace' && typeof c?.cycle_index === 'number')
209
+ .map(c => c.cycle_index as number)
210
+ .sort((a, b) => a - b);
211
+
212
+ if (cycleIdxs.length === 0) {
213
+ // Legacy run with a single un-cycled file → fall back
214
+ // to a one-shot read (server resolves the legacy path).
215
+ const resp: any = await invoke(topic as any, MessageType.Request, {
216
+ project_id: projectId, method_id: rowMethodId,
217
+ run_id: runId, name: 'trace',
218
+ } as any);
219
+ if (!resp?.success || !resp.data) {
220
+ console.warn(`${topic} returned no data for`, runId, resp?.error_message);
221
+ alert(`No ${label} available for ${runId}` +
222
+ (resp?.error_message ? `: ${resp.error_message}` : ''));
223
+ return;
224
+ }
225
+ csv = rawBlobToCsv(resp.data);
226
+ } else {
227
+ // Fetch each cycle's envelope in parallel; concat into
228
+ // one CSV with a leading cycle_index column.
229
+ const fetched = await Promise.all(cycleIdxs.map(async (ci) => {
230
+ const r: any = await invoke('tis.read_raw' as any, MessageType.Request, {
231
+ project_id: projectId, method_id: rowMethodId,
232
+ run_id: runId, name: 'trace', cycle_index: ci,
233
+ } as any);
234
+ return r?.success ? { cycleIndex: ci, blob: r.data } : null;
235
+ }));
236
+ const good = fetched.filter((x): x is { cycleIndex: number; blob: any } => x !== null);
237
+ if (good.length === 0) {
238
+ alert(`No ${label} cycles readable for ${runId}.`);
239
+ return;
240
+ }
241
+ csv = cyclesToCsv(good);
242
+ }
243
+ } else {
244
+ const resp: any = await invoke(topic as any, MessageType.Request, {
245
+ project_id: projectId, method_id: rowMethodId,
246
+ run_id: runId, name: 'trace',
247
+ } as any);
248
+ if (!resp?.success || !resp.data) {
249
+ console.warn(`${topic} returned no data for`, runId, resp?.error_message);
250
+ alert(`No ${label} available for ${runId}` +
251
+ (resp?.error_message ? `: ${resp.error_message}` : ''));
252
+ return;
253
+ }
254
+ csv = rawBlobToCsv(resp.data);
152
255
  }
153
256
 
154
- const csv = rawBlobToCsv(resp.data);
155
257
  if (!csv) {
156
258
  alert(`${label} for ${runId} is empty or has no array columns.`);
157
259
  return;
158
260
  }
159
- downloadCsv(`${projectId}_${rowMethodId}_${runId}_${suffix}.csv`, csv);
261
+ // sample_id is required for every run, so it always belongs in
262
+ // the filename. Sanitize to match the on-disk filename rules
263
+ // applied server-side in tis_servelet::sanitize_for_filename.
264
+ const sampleId = sanitizeForFilename(sampleIdOf(rowData));
265
+ const sampleSeg = sampleId ? `${sampleId}_` : '';
266
+ downloadCsv(`${projectId}_${rowMethodId}_${sampleSeg}${runId}_${suffix}.csv`, csv);
160
267
  } catch (err) {
161
268
  console.error(`Failed to download ${label}`, err);
162
269
  alert(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -179,6 +286,12 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
179
286
  return '';
180
287
  };
181
288
 
289
+ // Mirrors tis_servelet::sanitize_for_filename on the server. Keeping
290
+ // the rules aligned means the SampleID segment in download names
291
+ // matches the on-disk raw_data/<sample_id>_<name>.json file.
292
+ const sanitizeForFilename = (s: string): string =>
293
+ s.replace(/[\/\\:*?"<>|\0\x00-\x1f]/g, '_');
294
+
182
295
  return (
183
296
  // Outer wrapper pins the whole component to its container's width.
184
297
  // `overflow: hidden` keeps a wide row from pushing the table past the
@@ -109,7 +109,12 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
109
109
 
110
110
  // Lazy-loaded blobs for the View Raw Data dialog. Fetched only
111
111
  // when the dialog opens, and re-fetched if the operator pins a
112
- // different run while the dialog is closed.
112
+ // different run / cycle while the dialog is closed.
113
+ //
114
+ // Wire format (post per-cycle change) is an envelope:
115
+ // { cycle_index, cycle_fields, context, data: { col: number[] } }
116
+ // The legacy flat `{ col: number[] }` shape is still tolerated for
117
+ // runs written before the change — see unwrapEnvelope() below.
113
118
  const [rawBlob, setRawBlob] = useState<any | null>(null);
114
119
  const [rawError, setRawError] = useState<string | null>(null);
115
120
  const [rawLoading, setRawLoading] = useState(false);
@@ -117,6 +122,12 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
117
122
  const [filteredError, setFilteredError] = useState<string | null>(null);
118
123
  const [filteredLoading, setFilteredLoading] = useState(false);
119
124
 
125
+ // Per-test cycle listing for the raw dialog's cycle picker. Loaded
126
+ // once when the dialog opens. selectedCycle is null until the list
127
+ // arrives; we default it to the latest cycle.
128
+ const [availableCycles, setAvailableCycles] = useState<number[]>([]);
129
+ const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
130
+
120
131
  // Scatter-capable views only — raw_trace lives in <TestRawDataView>.
121
132
  const scatterViews = useMemo(() => {
122
133
  const out: { name: string; view: ChartView }[] = [];
@@ -295,14 +306,42 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
295
306
  // download in <ResultHistoryTable> pulls.
296
307
  // -----------------------------------------------------------------
297
308
  const blobName = schema?.raw_data?.blob_name ?? 'trace';
298
- const fetchKeyRef = useRef<string>(''); // last (project, method, run, blob) we fetched
309
+ const fetchKeyRef = useRef<string>(''); // last (project, method, run, blob, cycle) we fetched
310
+
311
+ // Discover which cycles have raw_data on disk so we can present a
312
+ // picker. tis.list_raw returns the flat name list (back-compat) AND
313
+ // a per-cycle list; we want the cycles for the selected blob name.
314
+ const loadCycleList = useCallback(async (): Promise<number[]> => {
315
+ if (!projectId || !methodId || !runId) return [];
316
+ try {
317
+ const resp: any = await invoke(
318
+ 'tis.list_raw' as any, MessageType.Request,
319
+ { project_id: projectId, method_id: methodId, run_id: runId } as any,
320
+ );
321
+ if (!resp?.success) return [];
322
+ const cycles: any[] = resp.data?.cycles ?? [];
323
+ const indices = cycles
324
+ .filter(c => c?.name === blobName && typeof c?.cycle_index === 'number')
325
+ .map(c => c.cycle_index as number)
326
+ .sort((a, b) => a - b);
327
+ return indices;
328
+ } catch {
329
+ return [];
330
+ }
331
+ }, [projectId, methodId, runId, blobName, invoke]);
299
332
 
300
- const loadBlobs = useCallback(async () => {
333
+ const loadBlobs = useCallback(async (cycleIndex: number | null) => {
301
334
  if (!projectId || !methodId || !runId) return;
302
- const key = `${projectId}|${methodId}|${runId}|${blobName}`;
303
- if (fetchKeyRef.current === key) return; // already loaded for this run
335
+ const key = `${projectId}|${methodId}|${runId}|${blobName}|${cycleIndex ?? 'latest'}`;
336
+ if (fetchKeyRef.current === key) return; // already loaded for this slice
304
337
  fetchKeyRef.current = key;
305
338
 
339
+ const baseArgs: Record<string, any> = {
340
+ project_id: projectId, method_id: methodId,
341
+ run_id: runId, name: blobName,
342
+ };
343
+ if (cycleIndex != null) baseArgs.cycle_index = cycleIndex;
344
+
306
345
  // Raw — must succeed for the dialog to be useful, but a
307
346
  // missing file is logged and surfaced rather than aborted.
308
347
  setRawLoading(true);
@@ -310,8 +349,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
310
349
  setRawBlob(null);
311
350
  try {
312
351
  const resp: any = await invoke(
313
- 'tis.read_raw' as any, MessageType.Request,
314
- { project_id: projectId, method_id: methodId, run_id: runId, name: blobName } as any,
352
+ 'tis.read_raw' as any, MessageType.Request, baseArgs as any,
315
353
  );
316
354
  if (resp?.success) {
317
355
  setRawBlob(resp.data ?? {});
@@ -327,6 +365,8 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
327
365
  // Filtered is optional. The 'no filtered data' case is the
328
366
  // common one (only the post-processing pipeline writes it),
329
367
  // and we render a friendly message rather than an error tone.
368
+ // Filtered files are not per-cycle (yet); cycle_index is
369
+ // ignored on the filtered side.
330
370
  setFilteredLoading(true);
331
371
  setFilteredError(null);
332
372
  setFilteredBlob(null);
@@ -347,18 +387,34 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
347
387
  }
348
388
  }, [projectId, methodId, runId, blobName, invoke]);
349
389
 
350
- // When the run changes, drop the cache key so the next dialog
351
- // open refetches. Don't auto-fetch — that's wasteful if the
352
- // operator never opens the dialog.
390
+ // When the run / blob changes, reset cycle picker and cache key.
353
391
  useEffect(() => {
354
392
  fetchKeyRef.current = '';
393
+ setAvailableCycles([]);
394
+ setSelectedCycle(null);
355
395
  }, [projectId, methodId, runId, blobName]);
356
396
 
357
- const openRawDialog = () => {
397
+ const openRawDialog = async () => {
358
398
  setRawOpen(true);
359
- void loadBlobs();
399
+ // Resolve cycle list lazily — first open after a run change.
400
+ let cycles = availableCycles;
401
+ if (cycles.length === 0) {
402
+ cycles = await loadCycleList();
403
+ setAvailableCycles(cycles);
404
+ }
405
+ const latest = cycles.length > 0 ? cycles[cycles.length - 1] : null;
406
+ const initial = selectedCycle ?? latest;
407
+ if (selectedCycle !== initial) setSelectedCycle(initial);
408
+ await loadBlobs(initial);
360
409
  };
361
410
 
411
+ // Re-fetch when the operator picks a different cycle from the dialog.
412
+ useEffect(() => {
413
+ if (rawOpen && selectedCycle != null) {
414
+ void loadBlobs(selectedCycle);
415
+ }
416
+ }, [rawOpen, selectedCycle, loadBlobs]);
417
+
362
418
  // -----------------------------------------------------------------
363
419
  // Render
364
420
  // -----------------------------------------------------------------
@@ -436,10 +492,16 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
436
492
  style={{ width: '90vw', height: '80vh' }}
437
493
  maximizable
438
494
  >
495
+ <CyclePickerBar
496
+ cycles={availableCycles}
497
+ selected={selectedCycle}
498
+ onChange={setSelectedCycle}
499
+ />
500
+ <RawEnvelopeHeader envelope={rawBlob} />
439
501
  <TabView style={{ height: '100%' }}>
440
502
  <TabPanel header="Raw Data">
441
503
  <DataBlobTable
442
- blob={rawBlob}
504
+ blob={unwrapEnvelope(rawBlob)}
443
505
  loading={rawLoading}
444
506
  error={rawError}
445
507
  rawData={schema.raw_data}
@@ -447,7 +509,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
447
509
  </TabPanel>
448
510
  <TabPanel header="Filtered Data">
449
511
  <DataBlobTable
450
- blob={filteredBlob}
512
+ blob={unwrapEnvelope(filteredBlob)}
451
513
  loading={filteredLoading}
452
514
  error={filteredError}
453
515
  rawData={schema.raw_data}
@@ -567,6 +629,90 @@ const ConfigList: React.FC<{ config: any }> = ({ config }) => {
567
629
  );
568
630
  };
569
631
 
632
+ // -------------------------------------------------------------------------
633
+ // unwrapEnvelope — `tis.read_raw` returns one of:
634
+ // - the per-cycle envelope `{ cycle_index, cycle_fields, context, data }`
635
+ // - the legacy flat columnar blob `{ col: number[] }` (pre per-cycle)
636
+ // Downstream consumers (DataBlobTable, chart datasets, CSV) only care
637
+ // about the columnar payload — peel off the envelope when present so
638
+ // both shapes render identically.
639
+ // -------------------------------------------------------------------------
640
+ const unwrapEnvelope = (blob: any): any => {
641
+ if (!blob || typeof blob !== 'object') return blob;
642
+ if ('data' in blob && blob.data && typeof blob.data === 'object'
643
+ && Object.values(blob.data).some(v => Array.isArray(v))) {
644
+ return blob.data;
645
+ }
646
+ return blob;
647
+ };
648
+
649
+ // -------------------------------------------------------------------------
650
+ // CyclePickerBar — dropdown of available cycle indices for the raw_data
651
+ // dialog. Hidden when only one (or zero) cycles exist; rendered as a
652
+ // labelled selector otherwise.
653
+ // -------------------------------------------------------------------------
654
+ const CyclePickerBar: React.FC<{
655
+ cycles: number[];
656
+ selected: number | null;
657
+ onChange: (idx: number) => void;
658
+ }> = ({ cycles, selected, onChange }) => {
659
+ if (cycles.length <= 1) return null;
660
+ return (
661
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem',
662
+ padding: '0.25rem 0.5rem 0.5rem' }}>
663
+ <label htmlFor="raw-cycle-picker"
664
+ style={{ color: 'var(--text-secondary-color)' }}>Cycle:</label>
665
+ <Dropdown
666
+ inputId="raw-cycle-picker"
667
+ value={selected}
668
+ options={cycles.map(c => ({ label: `Cycle ${c}`, value: c }))}
669
+ onChange={(e) => onChange(Number(e.value))}
670
+ style={{ minWidth: '8rem' }}
671
+ />
672
+ <span style={{ color: 'var(--text-secondary-color)' }}>
673
+ ({cycles.length} cycles recorded)
674
+ </span>
675
+ </div>
676
+ );
677
+ };
678
+
679
+ // -------------------------------------------------------------------------
680
+ // RawEnvelopeHeader — small key/value strip showing the metadata embedded
681
+ // in the on-disk envelope (cycle_index, schema-declared cycle_fields,
682
+ // capture context). Renders nothing for legacy flat blobs.
683
+ // -------------------------------------------------------------------------
684
+ const RawEnvelopeHeader: React.FC<{ envelope: any }> = ({ envelope }) => {
685
+ if (!envelope || typeof envelope !== 'object') return null;
686
+ const cycleIndex = envelope.cycle_index;
687
+ const cycleFields = envelope.cycle_fields;
688
+ const context = envelope.context;
689
+ if (cycleIndex == null && !cycleFields && !context) return null;
690
+
691
+ const renderKV = (obj: any) => {
692
+ if (!obj || typeof obj !== 'object') return null;
693
+ const entries = Object.entries(obj).filter(([, v]) =>
694
+ v !== null && typeof v !== 'object');
695
+ if (entries.length === 0) return null;
696
+ return entries.map(([k, v]) => (
697
+ <span key={k} style={{ marginRight: '1rem' }}>
698
+ <span style={{ color: 'var(--text-secondary-color)' }}>{k}: </span>
699
+ <span>{String(v)}</span>
700
+ </span>
701
+ ));
702
+ };
703
+
704
+ return (
705
+ <div style={{ padding: '0.5rem', borderBottom: '1px solid var(--surface-border)',
706
+ fontSize: '0.9rem' }}>
707
+ {cycleIndex != null && (
708
+ <div><strong>Cycle {cycleIndex}</strong></div>
709
+ )}
710
+ {cycleFields && <div style={{ marginTop: '0.25rem' }}>{renderKV(cycleFields)}</div>}
711
+ {context && <div style={{ marginTop: '0.25rem' }}>{renderKV(context)}</div>}
712
+ </div>
713
+ );
714
+ };
715
+
570
716
  // -------------------------------------------------------------------------
571
717
  // DataBlobTable — tabular display of a columnar JSON blob
572
718
  // (`{ col_name: number[] }`). Used by the View Raw Data dialog for both
@@ -56,10 +56,16 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
56
56
  const { invoke } = useContext(EventEmitterContext);
57
57
 
58
58
  const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
59
+ const [envelope, setEnvelope] = useState<any | null>(null);
59
60
  const [loading, setLoading] = useState(true);
60
61
  const [error, setError] = useState<string | null>(null);
61
62
  const chartRef = useRef<any>(null);
62
63
 
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
+
63
69
  // raw_trace-capable views only — cycle scatter lives in <TestDataView>.
64
70
  const traceViews = useMemo(() => {
65
71
  const out: { name: string; view: ChartView }[] = [];
@@ -75,10 +81,45 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
75
81
 
76
82
  const effectiveBlobName = blobName ?? schema?.raw_data?.blob_name ?? 'trace';
77
83
 
78
- // Lazy fetch only runs on mount / when identifiers change.
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.
79
120
  useEffect(() => {
80
121
  if (!projectId || !methodId || !runId) {
81
- setRaw(null); setLoading(false); setError(null);
122
+ setRaw(null); setEnvelope(null); setLoading(false); setError(null);
82
123
  return;
83
124
  }
84
125
  let cancelled = false;
@@ -86,13 +127,18 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
86
127
  setError(null);
87
128
  (async () => {
88
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;
89
135
  const resp: any = await invoke(
90
- 'tis.read_raw' as any, MessageType.Request as any,
91
- { project_id: projectId, method_id: methodId,
92
- run_id: runId, name: effectiveBlobName } as any);
136
+ 'tis.read_raw' as any, MessageType.Request as any, args as any);
93
137
  if (cancelled) return;
94
138
  if (resp?.success) {
95
- setRaw(resp.data ?? {});
139
+ const payload = resp.data ?? {};
140
+ setEnvelope(payload);
141
+ setRaw(unwrapEnvelope(payload));
96
142
  } else {
97
143
  setError(resp?.error_message ?? 'Failed to read raw data');
98
144
  }
@@ -103,7 +149,7 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
103
149
  }
104
150
  })();
105
151
  return () => { cancelled = true; };
106
- }, [projectId, methodId, runId, effectiveBlobName, invoke]);
152
+ }, [projectId, methodId, runId, effectiveBlobName, selectedCycle, invoke]);
107
153
 
108
154
  const chartData = useMemo(() => {
109
155
  if (!raw || !selectedView) return null;
@@ -174,7 +220,7 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
174
220
 
175
221
  return (
176
222
  <div className="vblock" style={{ display: 'flex', flexDirection: 'column', gap: '1rem', height: '100%' }}>
177
- <div className="flex" style={{ gap: '1rem', alignItems: 'center' }}>
223
+ <div className="flex" style={{ gap: '1rem', alignItems: 'center', flexWrap: 'wrap' }}>
178
224
  <Dropdown
179
225
  value={selectedView}
180
226
  options={traceViews.map(v => ({ label: v.view.title ?? v.name, value: v.name }))}
@@ -182,12 +228,30 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
182
228
  placeholder="Select a view"
183
229
  />
184
230
  <h3 style={{ margin: 0 }}>{selectedViewDef?.title ?? ''}</h3>
231
+ {cycles.length > 1 && (
232
+ <>
233
+ <label htmlFor="rawview-cycle-picker"
234
+ style={{ color: 'var(--text-secondary-color)' }}>Cycle:</label>
235
+ <Dropdown
236
+ inputId="rawview-cycle-picker"
237
+ value={selectedCycle}
238
+ options={cycles.map(c => ({ label: `Cycle ${c}`, value: c }))}
239
+ onChange={(e) => setSelectedCycle(Number(e.value))}
240
+ style={{ minWidth: '8rem' }}
241
+ />
242
+ <span style={{ color: 'var(--text-secondary-color)' }}>
243
+ of {cycles.length}
244
+ </span>
245
+ </>
246
+ )}
185
247
  <div style={{ flex: 1 }} />
186
248
  <Button icon="pi pi-refresh" label="Reset Zoom"
187
249
  outlined
188
250
  onClick={() => chartRef.current?.resetZoom?.()} />
189
251
  </div>
190
252
 
253
+ <EnvelopeMetaStrip envelope={envelope} />
254
+
191
255
  <div style={{ flex: 1, minHeight: 0, height: chartHeight, position: 'relative' }}>
192
256
  {loading && <Overlay>Loading raw data…</Overlay>}
193
257
  {error && <Overlay>{error}</Overlay>}
@@ -215,6 +279,52 @@ const EmptyState: React.FC<{ message: string }> = ({ message }) => (
215
279
  <div style={{ padding: '1rem', color: 'var(--text-secondary-color)' }}>{message}</div>
216
280
  );
217
281
 
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
+ // Strip of cycle_index + cycle_fields + context, rendered above the
297
+ // chart so the operator can see *which* cycle they're looking at and
298
+ // what schema-declared metric values were active for it. Renders
299
+ // nothing for legacy flat blobs (no envelope to read).
300
+ const EnvelopeMetaStrip: React.FC<{ envelope: any }> = ({ envelope }) => {
301
+ if (!envelope || typeof envelope !== 'object') return null;
302
+ const ci = envelope.cycle_index;
303
+ const cf = envelope.cycle_fields;
304
+ const ctx = envelope.context;
305
+ if (ci == null && !cf && !ctx) return null;
306
+ const kv = (obj: any) => {
307
+ if (!obj || typeof obj !== 'object') return null;
308
+ return Object.entries(obj)
309
+ .filter(([, v]) => v !== null && typeof v !== 'object')
310
+ .map(([k, v]) => (
311
+ <span key={k} style={{ marginRight: '1rem' }}>
312
+ <span style={{ color: 'var(--text-secondary-color)' }}>{k}: </span>
313
+ <span>{String(v)}</span>
314
+ </span>
315
+ ));
316
+ };
317
+ return (
318
+ <div style={{ padding: '0.5rem 0.25rem', fontSize: '0.9rem',
319
+ borderTop: '1px solid var(--surface-border)',
320
+ borderBottom: '1px solid var(--surface-border)' }}>
321
+ {ci != null && <div><strong>Cycle {ci}</strong></div>}
322
+ {cf && <div style={{ marginTop: '0.25rem' }}>{kv(cf)}</div>}
323
+ {ctx && <div style={{ marginTop: '0.25rem' }}>{kv(ctx)}</div>}
324
+ </div>
325
+ );
326
+ };
327
+
218
328
  const CHART_COLORS = [
219
329
  '#4ea8de', '#f59e0b', '#22c55e', '#a855f7',
220
330
  '#ef4444', '#14b8a6', '#eab308', '#ec4899',
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useContext, useMemo } from 'react';
1
+ import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
2
2
  import { Button } from 'primereact/button';
3
3
  import { InputText } from 'primereact/inputtext';
4
4
  import { Dropdown } from 'primereact/dropdown';
@@ -38,6 +38,13 @@ export interface TestFieldDef {
38
38
  label?: string;
39
39
  /** Long-form guidance surfaced as a hover tooltip on an info icon. */
40
40
  description?: string;
41
+ /** Seed value applied when the operator selects this test method.
42
+ * For source-bound fields the default is written to the GM tag so
43
+ * the control program sees it; for non-source fields it's stashed
44
+ * straight into stagedConfig. Operator edits override per-stage,
45
+ * but the schema default never mutates — re-selecting the method
46
+ * re-applies the default. */
47
+ default?: any;
41
48
  }
42
49
 
43
50
  export interface TestMethod {
@@ -240,6 +247,40 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
240
247
  );
241
248
  const closeInfoDialog = () => setInfoDialog({ open: false, title: '', body: null });
242
249
 
250
+ // Apply schema-declared defaults when the operator picks a method.
251
+ // Source-bound fields write the default to GM (the control program
252
+ // is the consumer of record); non-source fields land directly in
253
+ // stagedConfig. We track the last method we seeded so subsequent
254
+ // re-renders don't clobber operator edits — only an actual method
255
+ // change re-applies the defaults.
256
+ const defaultsAppliedFor = useRef<string>('');
257
+ useEffect(() => {
258
+ if (!schema || !methodId) return;
259
+ if (defaultsAppliedFor.current === methodId) return;
260
+ defaultsAppliedFor.current = methodId;
261
+
262
+ setConfig((prev: any) => {
263
+ let next = prev;
264
+ for (const field of schema.config_fields) {
265
+ if (field.name === 'sample_id') continue;
266
+ if (field.default === undefined || field.default === null) continue;
267
+ if (next === prev) next = { ...prev };
268
+ next[field.name] = field.default;
269
+ if (field.source) {
270
+ // Mirror handleFieldChange: write to GM so the
271
+ // control program sees the default. Errors here are
272
+ // logged but non-fatal — the form still reflects
273
+ // the default locally.
274
+ void Promise.resolve()
275
+ .then(() => write(field.source!, field.default))
276
+ .catch(e => console.error(
277
+ `[TestSetupForm] Failed to seed default for ${field.name}:`, e));
278
+ }
279
+ }
280
+ return next;
281
+ });
282
+ }, [schema, methodId, write]);
283
+
243
284
  // Seed and live-update config_fields that declare a `source`.
244
285
  useEffect(() => {
245
286
  if (!schema) return;