@adcops/autocore-react 3.3.113 → 3.3.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/ScienceTable.d.ts +15 -0
- package/dist/components/tis/ScienceTable.d.ts.map +1 -1
- package/dist/components/tis/ScienceTable.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +10 -4
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/package.json +110 -110
- package/src/components/tis/ResultHistoryTable.tsx +141 -1
- package/src/components/tis/ScienceTable.tsx +71 -12
- package/src/components/tis/TestDataView.tsx +65 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResultHistoryTable.d.ts","sourceRoot":"","sources":["../../../src/components/tis/ResultHistoryTable.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"ResultHistoryTable.d.ts","sourceRoot":"","sources":["../../../src/components/tis/ResultHistoryTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkD,MAAM,OAAO,CAAC;AAWvE,OAAO,0BAA0B,CAAC;AAElC,MAAM,WAAW,uBAAuB;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAyCD,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAogBhE,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{Badge}from"primereact/badge";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";import{SampleSummaryPanel}from"./SampleSummaryPanel";import"./ResultHistoryTable.css";const downloadCsvBlob=(e,t)=>{const o=new Blob([t],{type:"text/csv;charset=utf-8;"}),
|
|
1
|
+
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useRef}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Button}from"primereact/button";import{Badge}from"primereact/badge";import{Menu}from"primereact/menu";import{confirmDialog}from"primereact/confirmdialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";import{SampleSummaryPanel}from"./SampleSummaryPanel";import"./ResultHistoryTable.css";const downloadCsvBlob=(e,t)=>{const o=new Blob([t],{type:"text/csv;charset=utf-8;"}),i=URL.createObjectURL(o),s=document.createElement("a");s.href=i,s.download=e,document.body.appendChild(s),s.click(),s.remove(),URL.revokeObjectURL(i)},downloadViaUrl=(e,t)=>{const o=document.createElement("a");o.href=e,o.download=t,document.body.appendChild(o),o.click(),o.remove()};export const ResultHistoryTable=e=>{const t=useTis(),o=e.projectId??t.selection.projectId,i=e.methodId,[s,r]=useState([]),[a,n]=useState(!1),[l,d]=useState(null),[c,p]=useState(null),[u,m]=useState(null),f=useRef(null),[_,h]=useState(null),[y,b]=useState(null),{invoke:g}=useContext(EventEmitterContext),x=async()=>{if(o){n(!0);try{const e={project_id:o};i&&(e.method_id=i);const t=await g("tis.list_tests",MessageType.Request,e);t.success&&t.data&&t.data.tests&&r(t.data.tests)}catch(e){}n(!1)}else r([])};useEffect(()=>{x()},[o,i,t.state.activeRunId,t.state.active]);const v=async(e,t)=>{const s=e?.run_id,r=e?.method_id??i;if(!s||!r||!o)return;const a="report"===t?"tis.export_test_csv":"tis.export_test_data_csv",n="report"===t?"test report":"test data";d({runId:s,kind:t});try{const e=await g(a,MessageType.Request,{project_id:o,method_id:r,run_id:s});if(!e?.success)return void alert(`Failed to build ${n} for ${s}`+(e?.error_message?`: ${e.error_message}`:""));const i="string"==typeof e.data?.filename&&e.data.filename?e.data.filename:`${o}_${r}_${s}_${t}.csv`;if("report"===t){const t="string"==typeof e.data?.csv?e.data.csv:"";if(!t)return void alert(`${n} for ${s} is empty.`);downloadCsvBlob(i,t)}else{const t="string"==typeof e.data?.download_url?e.data.download_url:"";if(!t)return void alert(`Server did not return a download URL for ${n}.`);downloadViaUrl(t,i)}}catch(e){alert(`Download failed: ${e instanceof Error?e.message:String(e)}`)}finally{d(null)}},w=async(e,t)=>{const s=e?.run_id,r=e?.method_id??i;if(!s||!r||!o)return;const a="complete"===t?"tis.finish_test":"abandon"===t?"tis.reset_test":"tis.resume_test";b(s);try{const e=await g(a,MessageType.Request,{project_id:o,method_id:r,run_id:s});if(!e?.success)return void alert(`Could not ${t} ${s}`+(e?.error_message?`:\n\n${e.error_message}`:""));await x()}catch(e){alert(`${t} failed: ${e instanceof Error?e.message:String(e)}`)}finally{b(null)}},j=async e=>{if(!o)return;const t="report"===e?"tis.export_project_csv":"tis.export_project_zip",i="report"===e?"project report":"project archive";p(e);try{const s=await g(t,MessageType.Request,{project_id:o});if(!s?.success)return void alert(`Failed to build ${i}`+(s?.error_message?`: ${s.error_message}`:""));const r="string"==typeof s.data?.download_url?s.data.download_url:"";if(!r)return void alert(`Server did not return a download URL for ${i}.`);const a="report"===e?`${o}_project_report.csv`:`${o}_project_archive.zip`,n="string"==typeof s.data?.filename&&s.data.filename?s.data.filename:a;downloadViaUrl(r,n)}catch(e){alert(`Download failed: ${e instanceof Error?e.message:String(e)}`)}finally{p(null)}},C=e=>{const t="string"==typeof e?.sample_id?e.sample_id:"";if(t)return t;const o=e?.config;return o&&"object"==typeof o&&"string"==typeof o.sample_id?o.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",gap:"0.5rem",flexWrap:"wrap"},children:[_jsx("h3",{style:{margin:0},children:o?`Test History: ${o}${i?` / ${i}`:""}`:"Test History (no project selected)"}),_jsxs("div",{style:{display:"flex",gap:"0.4rem",alignItems:"center"},children:[_jsx(Button,{icon:"report"===c?"pi pi-spin pi-spinner":"pi pi-file",label:"Download Report",size:"small",outlined:!0,disabled:!o||null!==c,onClick:()=>j("report"),tooltip:"Download a CSV report of every test in this project",tooltipOptions:{position:"bottom"}}),_jsx(Button,{icon:"archive"===c?"pi pi-spin pi-spinner":"pi pi-box",label:"Download Archive",size:"small",outlined:!0,disabled:!o||null!==c,onClick:()=>j("archive"),tooltip:"Download a ZIP of the entire project directory (all tests, raw data, configs)",tooltipOptions:{position:"bottom"}}),_jsx(Button,{icon:"pi pi-refresh",label:"Refresh",size:"small",onClick:x,disabled:a})]})]}),_jsx(SampleSummaryPanel,{projectId:o,methodId:i}),_jsxs(DataTable,{value:s,loading:a,paginator:!0,rows:10,emptyMessage:"No tests found.",scrollable:!0,scrollHeight:"flex",tableStyle:{minWidth:0},style:{width:"100%"},rowClassName:e=>!0===e?.excluded?"tis-row-excluded":"",selectionMode:"single",onSelectionChange:e=>{const o=e.value;o?.run_id&&t.setSelection({projectId:o.project_id??null,methodId:o.method_id??null,runId:o.run_id})},children:[_jsx(Column,{header:"Sample ID",sortable:!0,body:C,sortFunction:e=>{const t=[...e.data];return t.sort((t,o)=>C(t).localeCompare(C(o))*(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,{header:"Status",field:"status",sortable:!0,body:e=>{const t=(e=>{if(!0===e?.finalized)return{label:"Abandoned",severity:"danger"};const t="string"==typeof e?.status?e.status:"completed";switch(t){case"in_progress":return{label:"In Progress",severity:"info"};case"paused":return{label:"Paused",severity:"warning"};case"completed":return{label:"Completed",severity:"success"};default:return{label:t,severity:void 0}}})(e);return _jsx(Badge,{value:t.label,severity:t.severity})},style:{minWidth:"6rem"}}),_jsx(Column,{header:"Incl.",align:"center",alignHeader:"center",style:{width:"4.5rem"},body:e=>{const t=!0===e?.excluded,s=u===e.run_id;return _jsx(Button,{icon:s?"pi pi-spin pi-spinner":t?"pi pi-eye-slash":"pi pi-check",rounded:!0,text:!0,size:"small",severity:t?"warning":"success",disabled:null!==u,onClick:()=>(async e=>{const t=e?.run_id,s=e?.method_id??i;if(!t||!s||!o)return;const r=!0!==e?.excluded;m(t);try{const e=await g("tis.set_test_excluded",MessageType.Request,{project_id:o,method_id:s,run_id:t,excluded:r});if(!e?.success)return void alert(`Failed to ${r?"exclude":"include"} ${t}`+(e?.error_message?`: ${e.error_message}`:""));await x()}catch(e){alert(`Exclude toggle failed: ${e instanceof Error?e.message:String(e)}`)}finally{m(null)}})(e),"aria-label":t?"Excluded — click to include":"Included — click to exclude",tooltip:t?"Excluded from cross-test results — click to include":"Included in cross-test results — click to exclude",tooltipOptions:{position:"left"}})}}),_jsx(Column,{header:"Download",style:{width:"14rem"},body:e=>{const t=l?.runId===e.run_id&&"data"===l?.kind,o=l?.runId===e.run_id&&"report"===l?.kind,i=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:i,onClick:()=>v(e,"data"),tooltip:"Download raw + filtered trace data as one CSV",tooltipOptions:{position:"left"}}),_jsx(Button,{icon:o?"pi pi-spin pi-spinner":"pi pi-file",label:"Results",size:"small",outlined:!0,disabled:i,onClick:()=>v(e,"report"),tooltip:"Download a CSV report (metadata + cycles + results) for this test",tooltipOptions:{position:"left"}})]})}}),_jsx(Column,{header:"",style:{width:"3rem"},body:e=>_jsx(Button,{icon:y===e.run_id?"pi pi-spin pi-spinner":"pi pi-ellipsis-v",text:!0,rounded:!0,size:"small",disabled:null!==y,"aria-label":"Test actions",tooltip:"Complete / abandon / reopen this test",tooltipOptions:{position:"left"},onClick:t=>{h(e),f.current?.toggle(t)}})})]}),_jsx(Menu,{model:_?(e=>{const t=e?.status,o=!0===e?.finalized,i=[];return"in_progress"===t||"paused"===t?(i.push({label:"Complete Test",icon:"pi pi-check-circle",command:()=>w(e,"complete")}),i.push({label:"Abandon Test",icon:"pi pi-ban",command:()=>confirmDialog({header:"Abandon Test",message:`Mark ${e?.sample_id||e?.run_id} as abandoned? Recorded data is kept, but the test can no longer be reopened.`,icon:"pi pi-exclamation-triangle",acceptClassName:"p-button-danger",acceptLabel:"Abandon",accept:()=>w(e,"abandon")})})):"completed"!==t||o?i.push({label:o?"Abandoned — cannot reopen":"No actions available",icon:"pi pi-info-circle",disabled:!0}):i.push({label:"Reopen Test",icon:"pi pi-replay",command:()=>confirmDialog({header:"Reopen Test",message:`Reopen ${e?.sample_id||e?.run_id} so new cycles are added to it? Cycle numbering continues from where it stopped. This changes where data is recorded — it does not start the machine.`,icon:"pi pi-replay",acceptLabel:"Reopen",accept:()=>w(e,"reopen")})}),i})(_):[],popup:!0,ref:f})]})};
|
|
@@ -13,6 +13,14 @@ export interface ScienceColumn {
|
|
|
13
13
|
body?: (row: any) => React.ReactNode;
|
|
14
14
|
/** Optional per-column style (e.g. minWidth). */
|
|
15
15
|
style?: React.CSSProperties;
|
|
16
|
+
/**
|
|
17
|
+
* Operator-editable cell. Requires the table's `onCellEdit`. Intended for
|
|
18
|
+
* ANNOTATION columns only (a specimen or lot identifier noted against an
|
|
19
|
+
* automatically-captured row) — never for measured values, which the server
|
|
20
|
+
* refuses to rewrite anyway (`tis.update_cycle` only accepts cycle_fields
|
|
21
|
+
* declared `editable: true`).
|
|
22
|
+
*/
|
|
23
|
+
editable?: boolean;
|
|
16
24
|
}
|
|
17
25
|
export interface ScienceTableProps {
|
|
18
26
|
/** Title shown in the dark bar above the table. Omit for no bar. */
|
|
@@ -29,6 +37,13 @@ export interface ScienceTableProps {
|
|
|
29
37
|
scrollHeight?: string;
|
|
30
38
|
/** Virtual-scroll row height in px. Default 38. */
|
|
31
39
|
virtualItemSize?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Commit handler for cells in columns marked `editable`. Called with the
|
|
42
|
+
* edited row, the column field and the new value. Without it, `editable`
|
|
43
|
+
* columns stay read-only — there is no local-only edit mode, because a value
|
|
44
|
+
* that looks saved but isn't is worse than one that can't be typed.
|
|
45
|
+
*/
|
|
46
|
+
onCellEdit?: (row: any, field: string, value: any) => void | Promise<void>;
|
|
32
47
|
}
|
|
33
48
|
export declare const ScienceTable: React.FC<ScienceTableProps>;
|
|
34
49
|
export default ScienceTable;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScienceTable.d.ts","sourceRoot":"","sources":["../../../src/components/tis/ScienceTable.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ScienceTable.d.ts","sourceRoot":"","sources":["../../../src/components/tis/ScienceTable.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,oBAAoB,CAAC;AAE5B,MAAM,WAAW,aAAa;IAC1B,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC;IACrC,iDAAiD;IACjD,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oEAAoE;IACpE,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9E;AASD,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA8FpD,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import"./ScienceTable.css";const HeaderCell=({column:e})=>_jsxs("div",{className:"ac-sci-th",children:[_jsx("span",{className:"ac-sci-th-name",children:e.label}),e.units?_jsxs("span",{className:"ac-sci-th-unit",children:["[",e.units,"]"]}):null]});export const ScienceTable=({title:e,columns:a,rows:l,showIndex:s=!0,indexHeader:
|
|
1
|
+
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{InputText}from"primereact/inputtext";import"./ScienceTable.css";const HeaderCell=({column:e})=>_jsxs("div",{className:"ac-sci-th",children:[_jsx("span",{className:"ac-sci-th-name",children:e.label}),e.units?_jsxs("span",{className:"ac-sci-th-unit",children:["[",e.units,"]"]}):null]});export const ScienceTable=({title:e,columns:a,rows:l,showIndex:s=!0,indexHeader:t="#",emptyMessage:i="No data.",scrollable:c=!1,scrollHeight:r,virtualItemSize:n=38,onCellEdit:o})=>{const d=c&&!!r,m=!!o&&a.some(e=>e.editable);return _jsxs("div",{className:"ac-science-table-wrap",children:[null!=e&&_jsx("div",{className:"ac-science-table-title",children:e}),_jsx("div",{className:"ac-science-table-scroll",children:_jsxs(DataTable,{className:"ac-science-table",value:l,scrollable:c,scrollHeight:r,virtualScrollerOptions:d?{itemSize:n}:void 0,emptyMessage:i,editMode:m?"cell":void 0,children:[s?_jsx(Column,{header:t,headerClassName:"ac-sci-index",bodyClassName:"ac-sci-index",align:"center",alignHeader:"center",style:{width:"3.5rem",maxWidth:"3.5rem"},body:(e,a)=>a.rowIndex+1}):null,a.map(e=>{const a=!!o&&!!e.editable;return _jsx(Column,{field:e.field,header:_jsx(HeaderCell,{column:e}),align:e.align??"center",alignHeader:"center",style:e.style,bodyClassName:a?"ac-sci-cell ac-sci-cell-editable":"ac-sci-cell",body:e.body?a=>e.body(a):void 0,editor:a?e=>_jsx(InputText,{value:e.value??"",autoFocus:!0,style:{width:"100%"},onChange:a=>e.editorCallback?.(a.target.value),onKeyDown:e=>e.stopPropagation()}):void 0,onCellEditComplete:a?a=>{const l=a.rowData?.[e.field];let s=a.newValue;if("number"==typeof l&&""!==s&&null!=s){const e=Number(s);Number.isNaN(e)||(s=e)}s!==l&&o(a.rowData,e.field,s)}:void 0},e.field)})]})})]})};export default ScienceTable;
|
|
@@ -17,6 +17,12 @@ export interface TestFieldDef {
|
|
|
17
17
|
label: string;
|
|
18
18
|
value: any;
|
|
19
19
|
}>;
|
|
20
|
+
/** Operator-editable after recording — annotation columns only (a specimen
|
|
21
|
+
* or lot id noted against a captured row). Meaningful on `cycle_fields`;
|
|
22
|
+
* the server refuses `tis.update_cycle` for any field without it, so
|
|
23
|
+
* measured columns can never be typed over. Mirrors Rust
|
|
24
|
+
* `TestField::editable`. */
|
|
25
|
+
editable?: boolean;
|
|
20
26
|
}
|
|
21
27
|
export interface ChartAxis {
|
|
22
28
|
field?: string;
|
|
@@ -116,10 +122,10 @@ export interface TestConfiguration {
|
|
|
116
122
|
};
|
|
117
123
|
}
|
|
118
124
|
export interface TestMethod {
|
|
119
|
-
project_fields
|
|
120
|
-
config_fields
|
|
121
|
-
cycle_fields
|
|
122
|
-
results_fields
|
|
125
|
+
project_fields?: TestFieldDef[];
|
|
126
|
+
config_fields?: TestFieldDef[];
|
|
127
|
+
cycle_fields?: TestFieldDef[];
|
|
128
|
+
results_fields?: TestFieldDef[];
|
|
123
129
|
raw_data?: RawDataShape | null;
|
|
124
130
|
views?: {
|
|
125
131
|
[name: string]: ChartView;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestDataView.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestDataView.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAwE,MAAM,OAAO,CAAC;AAiC7F,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;IAChB;;;uEAGmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;sEAEkE;IAClE,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"TestDataView.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestDataView.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAwE,MAAM,OAAO,CAAC;AAiC7F,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;IAChB;;;uEAGmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;sEAEkE;IAClE,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;IAC3E;;;;iCAI6B;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;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;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IACxB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AACD,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,KAAK,GAAG,UAAU,CAAC;IAC5B,CAAC,EAAE,SAAS,CAAC;IACb,CAAC,EAAE,WAAW,EAAE,CAAC;IACjB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CAC3B;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;oDACoD;AACpD,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,QAAQ,CAAC,EAAE;QAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC;CACvC;AACD,MAAM,WAAW,UAAU;IAOvB,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAChC,aAAa,CAAC,EAAG,YAAY,EAAE,CAAC;IAChC,YAAY,CAAC,EAAI,YAAY,EAAE,CAAC;IAChC,cAAc,CAAC,EAAE,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;IACxB,oEAAoE;IACpE,cAAc,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,iBAAiB;IAC9B,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,CAAC,EAAG,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAM,MAAM,CAAC;IACnB,kEAAkE;IAClE,MAAM,CAAC,EAAK,UAAU,CAAC;IACvB,8EAA8E;IAC9E,UAAU,CAAC,EAAG,MAAM,CAAC;IACrB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;uEACmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAID,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAisBpD,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 annotationPlugin from"chartjs-plugin-annotation";import{Line}from"react-chartjs-2";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";import{useRawCycleData}from"./useRawCycleData";import{ScienceTable}from"./ScienceTable";ChartJS.register(CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend,zoomPlugin,annotationPlugin);export const TestDataView=e=>{const t=useTis(),r=e.projectId??t.selection.projectId,a=e.methodId??t.selection.methodId,n=e.runId??t.selection.runId,l=e.schema??(a?t.schemas[a]:void 0),{throttleMs:o=100,cycleTableHeight:s="400px",chartHeight:i="320px"}=e,{invoke:c,subscribe:d,unsubscribe:u}=useContext(EventEmitterContext),[m,p]=useState(null),[y,f]=useState([]),[h,g]=useState({}),[_,x]=useState({}),[b,j]=useState(!1),[v,w]=useState(!1),C=useRef(null),[S,T]=useState(null),[R,M]=useState(null),[L,I]=useState(!1),[E,O]=useState(null),[A,D]=useState(null),[N,k]=useState(!1),[F,H]=useState([]),[z,P]=useState(null),B=useMemo(()=>{const e=[];for(const[t,r]of Object.entries(l?.views??{}))e.push({name:t,view:r});return e},[l]),[V,$]=useState(B.length>0?B[0].name:null);useEffect(()=>{if(0===B.length)return;null!==V&&B.some(e=>e.name===V)||$(B[0].name)},[B,V]);const G=B.find(e=>e.name===V)?.view,W="raw_trace"===G?.type,q=useRef([]),U=useRef(null),J=useRef(null),Y=()=>{J.current||(J.current=setTimeout(()=>{if(J.current=null,q.current.length>0){const e=q.current;q.current=[],f(t=>[...e.slice().reverse(),...t])}U.current&&(g(U.current),U.current=null)},o))};useEffect(()=>{if(!r||!a||!n)return p(null),f([]),void g({});let e=!1;return(async()=>{try{const t=await c("tis.read_test",MessageType.Request,{project_id:r,method_id:a,run_id:n});!e&&t?.success&&(p(t.data),g(t.data.results??{}));const l=await c("tis.read_cycles",MessageType.Request,{project_id:r,method_id:a,run_id:n,offset:0,limit:200,order:"desc"});!e&&l?.success&&f(l.data.cycles??[])}catch(e){}})(),()=>{e=!0}},[r,a,n,c]),useEffect(()=>{const e=e=>e?.project_id===r&&e?.method_id===a&&e?.run_id===n,t=d("tis.cycle_added",t=>{e(t)&&t.cycle&&(q.current.push(t.cycle),Y())}),l=d("tis.results_updated",t=>{e(t)&&(U.current=t.results??{},Y())});return()=>{u(t),u(l),J.current&&(clearTimeout(J.current),J.current=null)}},[r,a,n,o]);const Z="string"==typeof m?.sample_id&&m.sample_id?m.sample_id:"string"==typeof m?.config?.sample_id?m.config.sample_id:"";useEffect(()=>{if(!r||!a||!Z)return void x({});let e=!1;(async()=>{try{const t=await c("tis.sample_summary",MessageType.Request,{project_id:r,method_id:a,sample_id:Z});!e&&t?.success&&x(t.data?.summary??{})}catch(e){}})();const t=d("tis.sample_summary_updated",e=>{e?.project_id===r&&e?.method_id===a&&e?.sample_id===Z&&x(e.summary??{})});return()=>{e=!0,u(t)}},[r,a,Z,c,d,u]);const K=useRawCycleData({projectId:r,methodId:a,runId:n,blobName:l?.raw_data?.blob_name??"trace",source:G?.source??"raw",enabled:W}),Q=useMemo(()=>{if(!G)return null;if("cycle_scatter"===G.type){const e=G.x.field;if(!e)return null;const t=[...y].reverse();return{labels:t.map(t=>t[e]),datasets:G.y.map((e,r)=>({label:e.label??e.field,data:t.map(t=>t[e.field]),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(r),backgroundColor:palette(r),tension:.1,pointRadius:2}))}}if("raw_trace"===G.type){if(!K.raw)return null;const e=G.x.column;if(!e)return null;const t=K.raw[e]??[],r=!1!==G.smooth;return{datasets:G.y.map((e,a)=>{const n=K.raw[e.column]??[],l=r?smoothTraceForDisplay(t,n,G.smoothWindow):n.map((e,r)=>({x:t[r],y:e}));return{label:e.label??e.column,data:l,yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(a),backgroundColor:palette(a),pointRadius:0,borderWidth:1.5,showLine:!0,...r?{cubicInterpolationMode:"monotone"}:{}}})}}return null},[G,y,K.raw]),X=G?.y.some(e=>"right"===e.y_axis)??!1,ee=useMemo(()=>{if(!W)return[];const e=K.envelope,t=e?.regions??e?.context?.regions;return Array.isArray(t)?t:[]},[W,K.envelope]),te=useMemo(()=>[...G?.regions??[],...ee],[G,ee]),re=useMemo(()=>{const e="raw_trace"===G?.type;return{responsive:!0,maintainAspectRatio:!1,parsing:!e&&void 0,scales:{x:e?{type:"linear",title:{display:!!G?.x.label,text:G?.x.label}}:{title:{display:!!G?.x.label,text:G?.x.label}},y:{position:"left",title:{display:!0,text:leftAxisLabel(G)}},...X?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:rightAxisLabel(G)}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},mode:"xy"}},annotation:{annotations:buildRegionAnnotations(te)}}}},[G,X,te]),ae=l?.raw_data?.blob_name??"trace",ne=useRef(""),le=useCallback(async()=>{if(!r||!a||!n)return[];try{const e=await c("tis.list_raw",MessageType.Request,{project_id:r,method_id:a,run_id:n});if(!e?.success)return[];const t=e.data?.cycles??[];return t.filter(e=>e?.name===ae&&"number"==typeof e?.cycle_index).map(e=>e.cycle_index).sort((e,t)=>e-t)}catch{return[]}},[r,a,n,ae,c]),oe=useCallback(async e=>{if(!r||!a||!n)return;const t=`${r}|${a}|${n}|${ae}|${e??"latest"}`;if(ne.current===t)return;ne.current=t;const l={project_id:r,method_id:a,run_id:n,name:ae};null!=e&&(l.cycle_index=e),I(!0),M(null),T(null);try{const e=await c("tis.read_raw",MessageType.Request,l);e?.success?T(e.data??{}):M(e?.error_message??"No raw data on disk for this run.")}catch(e){M(String(e?.message??e))}finally{I(!1)}k(!0),D(null),O(null);try{const e=await c("tis.read_filtered",MessageType.Request,l);e?.success?O(e.data??{}):D(e?.error_message??"No filtered data on disk for this run.")}catch(e){D(String(e?.message??e))}finally{k(!1)}},[r,a,n,ae,c]);useEffect(()=>{ne.current="",H([]),P(null)},[r,a,n,ae]);return useEffect(()=>{b&&null!=z&&oe(z)},[b,z,oe]),r&&a&&n&&l?_jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsx(Header,{meta:m,config:m?.config,runId:n,projectId:r,methodId:a,canViewRaw:!!l.raw_data,onViewRaw:async()=>{j(!0);let e=F;0===e.length&&(e=await le(),H(e));const t=e.length>0?e[e.length-1]:null,r=z??t;z!==r&&P(r),await oe(r)},onShowConfig:()=>w(!0)}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsxs("div",{className:"flex",style:{gap:"1rem",alignItems:"center",marginBottom:"0.5rem",flexWrap:"wrap"},children:[_jsx(Dropdown,{value:V,options:B.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>$(e.value),placeholder:0===B.length?"No view defined":"Select a view",disabled:0===B.length}),_jsx("h3",{style:{margin:0},children:G?.title??""}),W&&K.cycles.length>1&&_jsxs(_Fragment,{children:[_jsx("label",{htmlFor:"chart-cycle-picker",style:{color:"var(--text-secondary-color)"},children:"Cycle:"}),_jsx(Dropdown,{inputId:"chart-cycle-picker",value:K.selectedCycle,options:K.cycles.map(e=>({label:`Cycle ${e}`,value:e})),onChange:e=>K.setSelectedCycle(Number(e.value)),style:{minWidth:"8rem"}}),_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:["of ",K.cycles.length]})]}),_jsx("div",{style:{flex:1}}),_jsx(Button,{icon:"pi pi-th-large",outlined:!0,rounded:!0,size:"small",onClick:()=>C.current?.resetZoom?.(),disabled:!Q,tooltip:"Reset chart zoom",tooltipOptions:{position:"left"},"aria-label":"Reset chart zoom"})]}),_jsxs("div",{style:{height:i,position:"relative"},children:[W&&K.loading&&_jsx(ChartOverlay,{children:"Loading raw data…"}),W&&K.error&&_jsx(ChartOverlay,{children:K.error}),Q&&_jsx(Line,{ref:C,data:Q,options:re})]})]}),(()=>{const e=y.length>CYCLE_VIRTUAL_THRESHOLD,t=l.cycle_fields.map(e=>({field:e.name,label:e.name,units:e.units,body:t=>formatCell(t[e.name],e.type,e.scale)}));return _jsx(ScienceTable,{title:`Cycle Data (${y.length})`,columns:t,rows:y,scrollable:e,scrollHeight:e?s:void 0,emptyMessage:"No cycles yet."})})(),(()=>{const e={...h,..._},t=Object.keys(e).length>0,r=l.results_fields.map(e=>({field:e.name,label:e.name,units:e.units,body:t=>formatCell(t[e.name],e.type,e.scale)}));return _jsx(ScienceTable,{title:"Results",columns:r,rows:t?[e]:[],emptyMessage:"No results yet."})})(),l.raw_data&&_jsxs(Dialog,{visible:b,onHide:()=>j(!1),header:`Run Data — ${n}`,style:{width:"90vw",height:"80vh"},maximizable:!0,children:[_jsx(CyclePickerBar,{cycles:F,selected:z,onChange:P}),_jsx(RawEnvelopeHeader,{envelope:S}),_jsxs(TabView,{style:{height:"100%"},children:[_jsx(TabPanel,{header:"Raw Data",children:_jsx(DataBlobTable,{blob:unwrapEnvelope(S),loading:L,error:R,rawData:l.raw_data})}),_jsx(TabPanel,{header:"Filtered Data",children:_jsx(DataBlobTable,{blob:unwrapEnvelope(E),loading:N,error:A,rawData:l.raw_data,emptyMessage:"Filtered data is written by post-processing — none on disk for this run yet."})})]})]}),_jsx(Dialog,{visible:v,onHide:()=>w(!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:n,canViewRaw:l,onViewRaw:o,onShowConfig:s})=>{const i="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:[i||r,c&&_jsx(Button,{icon:"pi pi-info-circle",type:"button",rounded:!0,text:!0,onClick:s,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: ",n," · 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:o,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 n=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:n(r)}),a&&_jsx("div",{style:{marginTop:"0.25rem"},children:n(a)})]})},DataBlobTable=({blob:e,loading:t,error:r,rawData:a,emptyMessage:n})=>{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:n??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??{}),o=Object.keys(e).filter(t=>Array.isArray(e[t])),s=[];for(const t of l)Array.isArray(e[t])&&s.push(t);for(const e of o)s.includes(e)||s.push(e);if(0===s.length)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:n??"No columnar data in this blob."});const i=s.reduce((t,r)=>Math.min(t,e[r].length),Number.POSITIVE_INFINITY),c=Number.isFinite(i)?i:0,d=Array.from({length:c},(t,r)=>{const a={__i:r};for(const t of s)a[t]=e[t][r];return a}),u=e=>{const t=a.units?.[e];return t?`${e} [${t}]`:e};return _jsxs(DataTable,{value:d,scrollable:!0,scrollHeight:"60vh",virtualScrollerOptions:{itemSize:32},emptyMessage:n??"No data.",size:"small",stripedRows:!0,children:[_jsx(Column,{field:"__i",header:"#",style:{width:"5rem",textAlign:"right"},bodyStyle:{fontVariantNumeric:"tabular-nums",textAlign:"right"}}),s.map(e=>_jsx(Column,{field:e,header:u(e),style:{minWidth:"8rem"},bodyStyle:{fontVariantNumeric:"tabular-nums",textAlign:"right"},body:t=>formatNumeric(t[e])},e))]})},formatNumeric=e=>null==e?"":"number"==typeof e&&Number.isFinite(e)?Number.parseFloat(e.toPrecision(6)).toString():String(e),CYCLE_VIRTUAL_THRESHOLD=30,CHART_COLORS=["#4ea8de","#f59e0b","#22c55e","#a855f7","#ef4444","#14b8a6","#eab308","#ec4899"],palette=e=>CHART_COLORS[e%CHART_COLORS.length],SG_ORDER=3,SMOOTH_TARGET_POINTS=800;function invertMatrix(e){const t=e.length,r=e.map((e,r)=>[...e,...Array.from({length:t},(e,t)=>r===t?1:0)]);for(let e=0;e<t;e++){let a=e;for(let n=e+1;n<t;n++)Math.abs(r[n][e])>Math.abs(r[a][e])&&(a=n);if(Math.abs(r[a][e])<1e-12)return null;[r[e],r[a]]=[r[a],r[e]];const n=r[e][e];for(let a=0;a<2*t;a++)r[e][a]/=n;for(let a=0;a<t;a++){if(a===e)continue;const n=r[a][e];if(0!==n)for(let l=0;l<2*t;l++)r[a][l]-=n*r[e][l]}}return r.map(e=>e.slice(t))}function sgWeights(e,t){const r=2*e+1,a=Math.min(t,r-1),n=[];for(let t=-e;t<=e;t++){const e=[];let r=1;for(let n=0;n<=a;n++)e.push(r),r*=t;n.push(e)}const l=a+1,o=Array.from({length:l},()=>new Array(l).fill(0));for(let e=0;e<l;e++)for(let t=0;t<l;t++){let a=0;for(let l=0;l<r;l++)a+=n[l][e]*n[l][t];o[e][t]=a}const s=invertMatrix(o);if(!s)return new Array(r).fill(1/r);const i=new Array(r);for(let e=0;e<r;e++){let t=0;for(let r=0;r<l;r++)t+=s[0][r]*n[e][r];i[e]=t}return i}function savitzkyGolay(e,t){const r=e.length;if(0===r||t<1)return e.slice();const a=new Map,n=e=>{let t=a.get(e);return t||(t=sgWeights(e,3),a.set(e,t)),t},l=new Array(r);for(let a=0;a<r;a++){const o=Math.min(t,a,r-1-a);if(o<1){l[a]=e[a];continue}const s=n(o);let i=0;for(let t=-o;t<=o;t++)i+=s[t+o]*e[a+t];l[a]=i}return l}function smoothTraceForDisplay(e,t,r){const a=Math.min(e.length,t.length);if(0===a)return[];const n=r&&r>1?Math.max(1,Math.floor((Math.min(r,a)-1)/2)):Math.max(2,Math.min(40,Math.round(a/250))),l=savitzkyGolay(t.slice(0,a),n),o=Math.max(1,Math.ceil(a/SMOOTH_TARGET_POINTS)),s=[];for(let t=0;t<a;t+=o)s.push({x:e[t],y:l[t]});return(a-1)%o!=0&&s.push({x:e[a-1],y:l[a-1]}),s}const DEFAULT_REGION_FILL="rgba(78, 168, 222, 0.15)",REGION_LABEL_COLOR="rgba(226, 232, 240, 0.85)";function buildRegionAnnotations(e){if(!e||0===e.length)return{};const t={};return e.forEach((e,r)=>{const a=!!e.borderColor;t[`region-${r}`]={type:"box",xScaleID:"x",xMin:e.xMin,xMax:e.xMax,backgroundColor:e.color??DEFAULT_REGION_FILL,borderColor:a?e.borderColor:"transparent",borderWidth:a?1:0,drawTime:"beforeDatasetsDraw",...e.label?{label:{display:!0,content:e.label,position:{x:"center",y:"start"},color:REGION_LABEL_COLOR,font:{size:11}}}:{}}}),t}const ChartOverlay=({children:e})=>_jsx("div",{style:{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",color:"var(--text-secondary-color)",pointerEvents:"none"},children:e}),seriesLabel=e=>e.label??e.field??e.column??"",leftAxisLabel=e=>e?.y.filter(e=>"right"!==e.y_axis).map(seriesLabel).join(" / ")??"",rightAxisLabel=e=>e?.y.filter(e=>"right"===e.y_axis).map(seriesLabel).join(" / ")??"",formatCell=(e,t,r)=>null==e?"":(r&&1!==r&&"number"==typeof e&&Number.isFinite(e)&&(e*=r),"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 annotationPlugin from"chartjs-plugin-annotation";import{Line}from"react-chartjs-2";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useTis}from"./TisProvider";import{useRawCycleData}from"./useRawCycleData";import{ScienceTable}from"./ScienceTable";ChartJS.register(CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend,zoomPlugin,annotationPlugin);export const TestDataView=e=>{const t=useTis(),r=e.projectId??t.selection.projectId,a=e.methodId??t.selection.methodId,n=e.runId??t.selection.runId,l=e.schema??(a?t.schemas[a]:void 0),{throttleMs:o=100,cycleTableHeight:s="400px",chartHeight:i="320px"}=e,{invoke:c,subscribe:d,unsubscribe:u}=useContext(EventEmitterContext),[m,p]=useState(null),[y,f]=useState([]),[h,g]=useState({}),[_,x]=useState({}),[b,j]=useState(!1),[v,w]=useState(!1),C=useCallback(async(e,t,l)=>{const o=e?.cycle_index;if(r&&a&&n&&o)try{const e=await c("tis.update_cycle",MessageType.Request,{project_id:r,method_id:a,run_id:n,cycle_index:o,fields:{[t]:l}});if(!1===e?.success)return;f(e=>e.map(e=>e?.cycle_index===o?{...e,[t]:l}:e))}catch(e){}},[c,r,a,n]),S=useRef(null),[T,R]=useState(null),[M,L]=useState(null),[E,I]=useState(!1),[O,A]=useState(null),[D,N]=useState(null),[k,F]=useState(!1),[H,z]=useState([]),[P,B]=useState(null),V=useMemo(()=>{const e=[];for(const[t,r]of Object.entries(l?.views??{}))e.push({name:t,view:r});return e},[l]),[$,G]=useState(V.length>0?V[0].name:null);useEffect(()=>{if(0===V.length)return;null!==$&&V.some(e=>e.name===$)||G(V[0].name)},[V,$]);const W=V.find(e=>e.name===$)?.view,q="raw_trace"===W?.type,U=useRef([]),J=useRef(null),Y=useRef(null),Z=()=>{Y.current||(Y.current=setTimeout(()=>{if(Y.current=null,U.current.length>0){const e=U.current;U.current=[],f(t=>[...e.slice().reverse(),...t])}J.current&&(g(J.current),J.current=null)},o))};useEffect(()=>{if(!r||!a||!n)return p(null),f([]),void g({});let e=!1;return(async()=>{try{const t=await c("tis.read_test",MessageType.Request,{project_id:r,method_id:a,run_id:n});!e&&t?.success&&(p(t.data),g(t.data.results??{}));const l=await c("tis.read_cycles",MessageType.Request,{project_id:r,method_id:a,run_id:n,offset:0,limit:200,order:"desc"});!e&&l?.success&&f(l.data.cycles??[])}catch(e){}})(),()=>{e=!0}},[r,a,n,c]),useEffect(()=>{const e=e=>e?.project_id===r&&e?.method_id===a&&e?.run_id===n,t=d("tis.cycle_added",t=>{e(t)&&t.cycle&&(U.current.push(t.cycle),Z())}),l=d("tis.results_updated",t=>{e(t)&&(J.current=t.results??{},Z())});return()=>{u(t),u(l),Y.current&&(clearTimeout(Y.current),Y.current=null)}},[r,a,n,o]);const K="string"==typeof m?.sample_id&&m.sample_id?m.sample_id:"string"==typeof m?.config?.sample_id?m.config.sample_id:"";useEffect(()=>{if(!r||!a||!K)return void x({});let e=!1;(async()=>{try{const t=await c("tis.sample_summary",MessageType.Request,{project_id:r,method_id:a,sample_id:K});!e&&t?.success&&x(t.data?.summary??{})}catch(e){}})();const t=d("tis.sample_summary_updated",e=>{e?.project_id===r&&e?.method_id===a&&e?.sample_id===K&&x(e.summary??{})});return()=>{e=!0,u(t)}},[r,a,K,c,d,u]);const Q=useRawCycleData({projectId:r,methodId:a,runId:n,blobName:l?.raw_data?.blob_name??"trace",source:W?.source??"raw",enabled:q}),X=useMemo(()=>{if(!W)return null;if("cycle_scatter"===W.type){const e=W.x.field;if(!e)return null;const t=[...y].reverse();return{labels:t.map(t=>t[e]),datasets:W.y.map((e,r)=>({label:e.label??e.field,data:t.map(t=>t[e.field]),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(r),backgroundColor:palette(r),tension:.1,pointRadius:2}))}}if("raw_trace"===W.type){if(!Q.raw)return null;const e=W.x.column;if(!e)return null;const t=Q.raw[e]??[],r=!1!==W.smooth;return{datasets:W.y.map((e,a)=>{const n=Q.raw[e.column]??[],l=r?smoothTraceForDisplay(t,n,W.smoothWindow):n.map((e,r)=>({x:t[r],y:e}));return{label:e.label??e.column,data:l,yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(a),backgroundColor:palette(a),pointRadius:0,borderWidth:1.5,showLine:!0,...r?{cubicInterpolationMode:"monotone"}:{}}})}}return null},[W,y,Q.raw]),ee=W?.y.some(e=>"right"===e.y_axis)??!1,te=useMemo(()=>{if(!q)return[];const e=Q.envelope,t=e?.regions??e?.context?.regions;return Array.isArray(t)?t:[]},[q,Q.envelope]),re=useMemo(()=>[...W?.regions??[],...te],[W,te]),ae=useMemo(()=>{const e="raw_trace"===W?.type;return{responsive:!0,maintainAspectRatio:!1,parsing:!e&&void 0,scales:{x:e?{type:"linear",title:{display:!!W?.x.label,text:W?.x.label}}:{title:{display:!!W?.x.label,text:W?.x.label}},y:{position:"left",title:{display:!0,text:leftAxisLabel(W)}},...ee?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:rightAxisLabel(W)}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},mode:"xy"}},annotation:{annotations:buildRegionAnnotations(re)}}}},[W,ee,re]),ne=l?.raw_data?.blob_name??"trace",le=useRef(""),oe=useCallback(async()=>{if(!r||!a||!n)return[];try{const e=await c("tis.list_raw",MessageType.Request,{project_id:r,method_id:a,run_id:n});if(!e?.success)return[];const t=e.data?.cycles??[];return t.filter(e=>e?.name===ne&&"number"==typeof e?.cycle_index).map(e=>e.cycle_index).sort((e,t)=>e-t)}catch{return[]}},[r,a,n,ne,c]),se=useCallback(async e=>{if(!r||!a||!n)return;const t=`${r}|${a}|${n}|${ne}|${e??"latest"}`;if(le.current===t)return;le.current=t;const l={project_id:r,method_id:a,run_id:n,name:ne};null!=e&&(l.cycle_index=e),I(!0),L(null),R(null);try{const e=await c("tis.read_raw",MessageType.Request,l);e?.success?R(e.data??{}):L(e?.error_message??"No raw data on disk for this run.")}catch(e){L(String(e?.message??e))}finally{I(!1)}F(!0),N(null),A(null);try{const e=await c("tis.read_filtered",MessageType.Request,l);e?.success?A(e.data??{}):N(e?.error_message??"No filtered data on disk for this run.")}catch(e){N(String(e?.message??e))}finally{F(!1)}},[r,a,n,ne,c]);useEffect(()=>{le.current="",z([]),B(null)},[r,a,n,ne]);return useEffect(()=>{b&&null!=P&&se(P)},[b,P,se]),r&&a&&n&&l?_jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsx(Header,{meta:m,config:m?.config,runId:n,projectId:r,methodId:a,canViewRaw:!!l.raw_data,onViewRaw:async()=>{j(!0);let e=H;0===e.length&&(e=await oe(),z(e));const t=e.length>0?e[e.length-1]:null,r=P??t;P!==r&&B(r),await se(r)},onShowConfig:()=>w(!0)}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsxs("div",{className:"flex",style:{gap:"1rem",alignItems:"center",marginBottom:"0.5rem",flexWrap:"wrap"},children:[_jsx(Dropdown,{value:$,options:V.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>G(e.value),placeholder:0===V.length?"No view defined":"Select a view",disabled:0===V.length}),_jsx("h3",{style:{margin:0},children:W?.title??""}),q&&Q.cycles.length>1&&_jsxs(_Fragment,{children:[_jsx("label",{htmlFor:"chart-cycle-picker",style:{color:"var(--text-secondary-color)"},children:"Cycle:"}),_jsx(Dropdown,{inputId:"chart-cycle-picker",value:Q.selectedCycle,options:Q.cycles.map(e=>({label:`Cycle ${e}`,value:e})),onChange:e=>Q.setSelectedCycle(Number(e.value)),style:{minWidth:"8rem"}}),_jsxs("span",{style:{color:"var(--text-secondary-color)"},children:["of ",Q.cycles.length]})]}),_jsx("div",{style:{flex:1}}),_jsx(Button,{icon:"pi pi-th-large",outlined:!0,rounded:!0,size:"small",onClick:()=>S.current?.resetZoom?.(),disabled:!X,tooltip:"Reset chart zoom",tooltipOptions:{position:"left"},"aria-label":"Reset chart zoom"})]}),_jsxs("div",{style:{height:i,position:"relative"},children:[q&&Q.loading&&_jsx(ChartOverlay,{children:"Loading raw data…"}),q&&Q.error&&_jsx(ChartOverlay,{children:Q.error}),X&&_jsx(Line,{ref:S,data:X,options:ae})]})]}),(()=>{const e=y.length>CYCLE_VIRTUAL_THRESHOLD,t=(l.cycle_fields??[]).map(e=>({field:e.name,label:e.name,units:e.units,editable:e.editable,body:t=>formatCell(t[e.name],e.type,e.scale)}));return _jsx(ScienceTable,{title:`Cycle Data (${y.length})`,columns:t,rows:y,scrollable:e,scrollHeight:e?s:void 0,emptyMessage:"No cycles yet.",onCellEdit:C})})(),(()=>{const e={...h,..._},t=Object.keys(e).length>0,r=(l.results_fields??[]).map(e=>({field:e.name,label:e.name,units:e.units,body:t=>formatCell(t[e.name],e.type,e.scale)}));return _jsx(ScienceTable,{title:"Results",columns:r,rows:t?[e]:[],emptyMessage:"No results yet."})})(),l.raw_data&&_jsxs(Dialog,{visible:b,onHide:()=>j(!1),header:`Run Data — ${n}`,style:{width:"90vw",height:"80vh"},maximizable:!0,children:[_jsx(CyclePickerBar,{cycles:H,selected:P,onChange:B}),_jsx(RawEnvelopeHeader,{envelope:T}),_jsxs(TabView,{style:{height:"100%"},children:[_jsx(TabPanel,{header:"Raw Data",children:_jsx(DataBlobTable,{blob:unwrapEnvelope(T),loading:E,error:M,rawData:l.raw_data})}),_jsx(TabPanel,{header:"Filtered Data",children:_jsx(DataBlobTable,{blob:unwrapEnvelope(O),loading:k,error:D,rawData:l.raw_data,emptyMessage:"Filtered data is written by post-processing — none on disk for this run yet."})})]})]}),_jsx(Dialog,{visible:v,onHide:()=>w(!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:n,canViewRaw:l,onViewRaw:o,onShowConfig:s})=>{const i="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:[i||r,c&&_jsx(Button,{icon:"pi pi-info-circle",type:"button",rounded:!0,text:!0,onClick:s,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: ",n," · 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:o,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 n=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:n(r)}),a&&_jsx("div",{style:{marginTop:"0.25rem"},children:n(a)})]})},DataBlobTable=({blob:e,loading:t,error:r,rawData:a,emptyMessage:n})=>{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:n??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??{}),o=Object.keys(e).filter(t=>Array.isArray(e[t])),s=[];for(const t of l)Array.isArray(e[t])&&s.push(t);for(const e of o)s.includes(e)||s.push(e);if(0===s.length)return _jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:n??"No columnar data in this blob."});const i=s.reduce((t,r)=>Math.min(t,e[r].length),Number.POSITIVE_INFINITY),c=Number.isFinite(i)?i:0,d=Array.from({length:c},(t,r)=>{const a={__i:r};for(const t of s)a[t]=e[t][r];return a}),u=e=>{const t=a.units?.[e];return t?`${e} [${t}]`:e};return _jsxs(DataTable,{value:d,scrollable:!0,scrollHeight:"60vh",virtualScrollerOptions:{itemSize:32},emptyMessage:n??"No data.",size:"small",stripedRows:!0,children:[_jsx(Column,{field:"__i",header:"#",style:{width:"5rem",textAlign:"right"},bodyStyle:{fontVariantNumeric:"tabular-nums",textAlign:"right"}}),s.map(e=>_jsx(Column,{field:e,header:u(e),style:{minWidth:"8rem"},bodyStyle:{fontVariantNumeric:"tabular-nums",textAlign:"right"},body:t=>formatNumeric(t[e])},e))]})},formatNumeric=e=>null==e?"":"number"==typeof e&&Number.isFinite(e)?Number.parseFloat(e.toPrecision(6)).toString():String(e),CYCLE_VIRTUAL_THRESHOLD=30,CHART_COLORS=["#4ea8de","#f59e0b","#22c55e","#a855f7","#ef4444","#14b8a6","#eab308","#ec4899"],palette=e=>CHART_COLORS[e%CHART_COLORS.length],SG_ORDER=3,SMOOTH_TARGET_POINTS=800;function invertMatrix(e){const t=e.length,r=e.map((e,r)=>[...e,...Array.from({length:t},(e,t)=>r===t?1:0)]);for(let e=0;e<t;e++){let a=e;for(let n=e+1;n<t;n++)Math.abs(r[n][e])>Math.abs(r[a][e])&&(a=n);if(Math.abs(r[a][e])<1e-12)return null;[r[e],r[a]]=[r[a],r[e]];const n=r[e][e];for(let a=0;a<2*t;a++)r[e][a]/=n;for(let a=0;a<t;a++){if(a===e)continue;const n=r[a][e];if(0!==n)for(let l=0;l<2*t;l++)r[a][l]-=n*r[e][l]}}return r.map(e=>e.slice(t))}function sgWeights(e,t){const r=2*e+1,a=Math.min(t,r-1),n=[];for(let t=-e;t<=e;t++){const e=[];let r=1;for(let n=0;n<=a;n++)e.push(r),r*=t;n.push(e)}const l=a+1,o=Array.from({length:l},()=>new Array(l).fill(0));for(let e=0;e<l;e++)for(let t=0;t<l;t++){let a=0;for(let l=0;l<r;l++)a+=n[l][e]*n[l][t];o[e][t]=a}const s=invertMatrix(o);if(!s)return new Array(r).fill(1/r);const i=new Array(r);for(let e=0;e<r;e++){let t=0;for(let r=0;r<l;r++)t+=s[0][r]*n[e][r];i[e]=t}return i}function savitzkyGolay(e,t){const r=e.length;if(0===r||t<1)return e.slice();const a=new Map,n=e=>{let t=a.get(e);return t||(t=sgWeights(e,3),a.set(e,t)),t},l=new Array(r);for(let a=0;a<r;a++){const o=Math.min(t,a,r-1-a);if(o<1){l[a]=e[a];continue}const s=n(o);let i=0;for(let t=-o;t<=o;t++)i+=s[t+o]*e[a+t];l[a]=i}return l}function smoothTraceForDisplay(e,t,r){const a=Math.min(e.length,t.length);if(0===a)return[];const n=r&&r>1?Math.max(1,Math.floor((Math.min(r,a)-1)/2)):Math.max(2,Math.min(40,Math.round(a/250))),l=savitzkyGolay(t.slice(0,a),n),o=Math.max(1,Math.ceil(a/SMOOTH_TARGET_POINTS)),s=[];for(let t=0;t<a;t+=o)s.push({x:e[t],y:l[t]});return(a-1)%o!=0&&s.push({x:e[a-1],y:l[a-1]}),s}const DEFAULT_REGION_FILL="rgba(78, 168, 222, 0.15)",REGION_LABEL_COLOR="rgba(226, 232, 240, 0.85)";function buildRegionAnnotations(e){if(!e||0===e.length)return{};const t={};return e.forEach((e,r)=>{const a=!!e.borderColor;t[`region-${r}`]={type:"box",xScaleID:"x",xMin:e.xMin,xMax:e.xMax,backgroundColor:e.color??DEFAULT_REGION_FILL,borderColor:a?e.borderColor:"transparent",borderWidth:a?1:0,drawTime:"beforeDatasetsDraw",...e.label?{label:{display:!0,content:e.label,position:{x:"center",y:"start"},color:REGION_LABEL_COLOR,font:{size:11}}}:{}}}),t}const ChartOverlay=({children:e})=>_jsx("div",{style:{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",color:"var(--text-secondary-color)",pointerEvents:"none"},children:e}),seriesLabel=e=>e.label??e.field??e.column??"",leftAxisLabel=e=>e?.y.filter(e=>"right"!==e.y_axis).map(seriesLabel).join(" / ")??"",rightAxisLabel=e=>e?.y.filter(e=>"right"===e.y_axis).map(seriesLabel).join(" / ")??"",formatCell=(e,t,r)=>null==e?"":(r&&1!==r&&"number"==typeof e&&Number.isFinite(e)&&(e*=r),"f32"===t||"f64"===t?"number"==typeof e?e.toFixed(4):String(e):"object"==typeof e?JSON.stringify(e):String(e));
|
package/package.json
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
2
|
+
"name": "@adcops/autocore-react",
|
|
3
|
+
"version": "3.3.115",
|
|
4
|
+
"description": "A React component library for industrial user interfaces.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "rimraf ./dist",
|
|
9
|
+
"minify": "node ./tools/minify.cjs",
|
|
10
|
+
"tsc": "tsc",
|
|
11
|
+
"build-themes": "node ./tools/build-themes.cjs",
|
|
12
|
+
"generate-docs": "typedoc",
|
|
13
|
+
"build": "npm run clean && tsc && node ./tools/copy-distribution-files.cjs && npm run build-themes && npm run minify",
|
|
14
|
+
"build:dev": "npm run clean && tsc && node ./tools/copy-distribution-files.cjs",
|
|
15
|
+
"dev": "vite",
|
|
16
|
+
"playground": "vite",
|
|
17
|
+
"playground:build": "vite build",
|
|
18
|
+
"prestart": "tsc",
|
|
19
|
+
"publish-package": "npm run build && npm version patch && npm publish",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"release:patch": "npm version patch && npm publish",
|
|
22
|
+
"release:minor": "npm version minor && npm publish",
|
|
23
|
+
"release:major": "npm version major && npm publish",
|
|
24
|
+
"convert-assets": "npx @svgr/cli --typescript --out-dir src/assets/ src/assets/svg/ --jsx-runtime automatic --ignore-existing"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://adc360.visualstudio.com/AutoCore-React/_git/autocore-react"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"react",
|
|
32
|
+
"autocore",
|
|
33
|
+
"component-library",
|
|
34
|
+
"industrial-ui",
|
|
35
|
+
"typescript",
|
|
36
|
+
"primereact"
|
|
37
|
+
],
|
|
38
|
+
"author": "Automated Design Corp.",
|
|
39
|
+
"license": "ISC",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://automateddesign.com"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://automateddesign.com",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@types/react": "^18",
|
|
46
|
+
"primereact": ">=10.9.7",
|
|
47
|
+
"react": "^18",
|
|
48
|
+
"react-dom": "^18"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@types/react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@monaco-editor/react": "^4.7.0",
|
|
57
|
+
"@tauri-apps/api": "^2.9.1",
|
|
58
|
+
"chart.js": "^4.5.1",
|
|
59
|
+
"chartjs-plugin-annotation": "^3.1.0",
|
|
60
|
+
"chartjs-plugin-zoom": "^2.2.0",
|
|
61
|
+
"clsx": "^2.1.1",
|
|
62
|
+
"numerable": "^0.3.15",
|
|
63
|
+
"react-blockly": "^8.1.2",
|
|
64
|
+
"react-chartjs-2": "^5.3.0",
|
|
65
|
+
"react-number-format": "^5.4.5",
|
|
66
|
+
"react-simple-keyboard": "^3.8.120",
|
|
67
|
+
"react-transition-group": "^4.4.5",
|
|
68
|
+
"sass": "^1.92.1",
|
|
69
|
+
"socket.io-client": "^4.8.1",
|
|
70
|
+
"use-fit-text": "^2.4.0"
|
|
71
|
+
},
|
|
72
|
+
"sideEffects": [
|
|
73
|
+
"**/*.css",
|
|
74
|
+
"**/*.scss"
|
|
75
|
+
],
|
|
76
|
+
"engines": {
|
|
77
|
+
"node": ">=20.0.0"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@svgr/cli": "^8.1.0",
|
|
81
|
+
"@types/react": "^18.2.15",
|
|
82
|
+
"@types/react-dom": "^18.2.7",
|
|
83
|
+
"@types/react-transition-group": "^4.4.12",
|
|
84
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
85
|
+
"primereact": "^10.9.7",
|
|
86
|
+
"rimraf": "^6.0.1",
|
|
87
|
+
"terser": "^5.44.0",
|
|
88
|
+
"typedoc": "^0.28.12",
|
|
89
|
+
"typescript": "^5.9.2",
|
|
90
|
+
"vite": "^8.0.13"
|
|
91
|
+
},
|
|
92
|
+
"exports": {
|
|
93
|
+
"./core/*": {
|
|
94
|
+
"import": "./dist/core/*",
|
|
95
|
+
"types": "./dist/core/*.d.ts"
|
|
49
96
|
},
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
97
|
+
"./components/*": {
|
|
98
|
+
"import": "./dist/components/*",
|
|
99
|
+
"types": "./dist/components/*.d.ts"
|
|
54
100
|
},
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"chart.js": "^4.5.1",
|
|
59
|
-
"chartjs-plugin-annotation": "^3.1.0",
|
|
60
|
-
"chartjs-plugin-zoom": "^2.2.0",
|
|
61
|
-
"clsx": "^2.1.1",
|
|
62
|
-
"numerable": "^0.3.15",
|
|
63
|
-
"react-blockly": "^8.1.2",
|
|
64
|
-
"react-chartjs-2": "^5.3.0",
|
|
65
|
-
"react-number-format": "^5.4.5",
|
|
66
|
-
"react-simple-keyboard": "^3.8.120",
|
|
67
|
-
"react-transition-group": "^4.4.5",
|
|
68
|
-
"sass": "^1.92.1",
|
|
69
|
-
"socket.io-client": "^4.8.1",
|
|
70
|
-
"use-fit-text": "^2.4.0"
|
|
101
|
+
"./assets/*": {
|
|
102
|
+
"import": "./dist/assets/*",
|
|
103
|
+
"types": "./dist/assets/*.d.ts"
|
|
71
104
|
},
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
],
|
|
76
|
-
"engines": {
|
|
77
|
-
"node": ">=20.0.0"
|
|
105
|
+
"./hooks/*": {
|
|
106
|
+
"import": "./dist/hooks/*",
|
|
107
|
+
"types": "./dist/hooks/*.d.ts"
|
|
78
108
|
},
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"@types/react-dom": "^18.2.7",
|
|
83
|
-
"@types/react-transition-group": "^4.4.12",
|
|
84
|
-
"@vitejs/plugin-react": "^6.0.2",
|
|
85
|
-
"primereact": "^10.9.7",
|
|
86
|
-
"rimraf": "^6.0.1",
|
|
87
|
-
"terser": "^5.44.0",
|
|
88
|
-
"typedoc": "^0.28.12",
|
|
89
|
-
"typescript": "^5.9.2",
|
|
90
|
-
"vite": "^8.0.13"
|
|
109
|
+
"./themes/*": {
|
|
110
|
+
"import": "./dist/themes/*",
|
|
111
|
+
"types": "./dist/themes/*.d.ts"
|
|
91
112
|
},
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"types": "./dist/core/*.d.ts"
|
|
96
|
-
},
|
|
97
|
-
"./components/*": {
|
|
98
|
-
"import": "./dist/components/*",
|
|
99
|
-
"types": "./dist/components/*.d.ts"
|
|
100
|
-
},
|
|
101
|
-
"./assets/*": {
|
|
102
|
-
"import": "./dist/assets/*",
|
|
103
|
-
"types": "./dist/assets/*.d.ts"
|
|
104
|
-
},
|
|
105
|
-
"./hooks/*": {
|
|
106
|
-
"import": "./dist/hooks/*",
|
|
107
|
-
"types": "./dist/hooks/*.d.ts"
|
|
108
|
-
},
|
|
109
|
-
"./themes/*": {
|
|
110
|
-
"import": "./dist/themes/*",
|
|
111
|
-
"types": "./dist/themes/*.d.ts"
|
|
112
|
-
},
|
|
113
|
-
"./hub/*": {
|
|
114
|
-
"import": "./dist/hub/*",
|
|
115
|
-
"types": "./dist/hub/*.d.ts"
|
|
116
|
-
}
|
|
113
|
+
"./hub/*": {
|
|
114
|
+
"import": "./dist/hub/*",
|
|
115
|
+
"types": "./dist/hub/*.d.ts"
|
|
117
116
|
}
|
|
117
|
+
}
|
|
118
118
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import React, { useState, useEffect, useContext } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useContext, useRef } from 'react';
|
|
2
2
|
import { DataTable } from 'primereact/datatable';
|
|
3
3
|
import { Column } from 'primereact/column';
|
|
4
4
|
import { Button } from 'primereact/button';
|
|
5
5
|
import { Badge } from 'primereact/badge';
|
|
6
|
+
import { Menu } from 'primereact/menu';
|
|
7
|
+
import { confirmDialog } from 'primereact/confirmdialog';
|
|
6
8
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
7
9
|
import { MessageType } from '../../hub/CommandMessage';
|
|
8
10
|
import { useTis } from './TisProvider';
|
|
@@ -75,6 +77,11 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
75
77
|
const [downloading, setDownloading] = useState<InFlight | null>(null);
|
|
76
78
|
const [projectBusy, setProjectBusy] = useState<ProjectDownloadKind | null>(null);
|
|
77
79
|
const [excludingRunId, setExcludingRunId] = useState<string | null>(null);
|
|
80
|
+
// Per-row lifecycle menu (complete / abandon / reopen). One Menu instance is
|
|
81
|
+
// reused; `menuRow` is the row it was opened for.
|
|
82
|
+
const rowMenuRef = useRef<Menu>(null);
|
|
83
|
+
const [menuRow, setMenuRow] = useState<any>(null);
|
|
84
|
+
const [lifecycleBusy, setLifecycleBusy] = useState<string | null>(null);
|
|
78
85
|
const { invoke } = useContext(EventEmitterContext);
|
|
79
86
|
|
|
80
87
|
const loadTests = async () => {
|
|
@@ -176,6 +183,112 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
176
183
|
// kept on disk (not deleted) but dropped from avg/min/max/stddev/count
|
|
177
184
|
// results for their sample group. The server recomputes + rebroadcasts
|
|
178
185
|
// the sample summary on success.
|
|
186
|
+
/**
|
|
187
|
+
* Run-lifecycle action on one row: complete, abandon, or reopen.
|
|
188
|
+
*
|
|
189
|
+
* All three are RECORD operations — they change what the stored run says
|
|
190
|
+
* about itself, never what the machine is doing. Reopening makes a run the
|
|
191
|
+
* active one so the control program's next cycle appends to it; it does not
|
|
192
|
+
* command motion, and the machine still decides when to run. That
|
|
193
|
+
* distinction is why this is safe to expose in a browser: a click here can
|
|
194
|
+
* retarget where data lands, but cannot start a machine.
|
|
195
|
+
*
|
|
196
|
+
* `tis.finish_test` / `reset_test` / `resume_test` all take an explicit
|
|
197
|
+
* `run_id`, so the row's own run is addressed rather than "whatever is
|
|
198
|
+
* newest".
|
|
199
|
+
*/
|
|
200
|
+
const handleLifecycle = async (
|
|
201
|
+
rowData: any,
|
|
202
|
+
action: 'complete' | 'abandon' | 'reopen',
|
|
203
|
+
) => {
|
|
204
|
+
const runId = rowData?.run_id;
|
|
205
|
+
const rowMethodId = rowData?.method_id ?? methodId;
|
|
206
|
+
if (!runId || !rowMethodId || !projectId) return;
|
|
207
|
+
const topic =
|
|
208
|
+
action === 'complete' ? 'tis.finish_test'
|
|
209
|
+
: action === 'abandon' ? 'tis.reset_test'
|
|
210
|
+
: 'tis.resume_test';
|
|
211
|
+
setLifecycleBusy(runId);
|
|
212
|
+
try {
|
|
213
|
+
const resp: any = await invoke(topic as any, MessageType.Request, {
|
|
214
|
+
project_id: projectId, method_id: rowMethodId, run_id: runId,
|
|
215
|
+
} as any);
|
|
216
|
+
if (!resp?.success) {
|
|
217
|
+
// The most common rejection is reopening while another test is
|
|
218
|
+
// already open for this project+method — the server refuses so
|
|
219
|
+
// two runs can't both be "active".
|
|
220
|
+
alert(`Could not ${action} ${runId}` +
|
|
221
|
+
(resp?.error_message ? `:\n\n${resp.error_message}` : ''));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
await loadTests();
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(`Failed to ${action} test`, err);
|
|
227
|
+
alert(`${action} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
228
|
+
} finally {
|
|
229
|
+
setLifecycleBusy(null);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Menu items for a row, gated on its lifecycle state:
|
|
235
|
+
* - open (in_progress / paused) → Complete, Abandon
|
|
236
|
+
* - completed but not finalized → Reopen (add more cycles under the same
|
|
237
|
+
* Test ID — the multi-specimen case where a set turned out to be bigger)
|
|
238
|
+
* - finalized (abandoned) → nothing; `finalized` exists precisely to
|
|
239
|
+
* stop a deliberately-abandoned run being resurrected.
|
|
240
|
+
*/
|
|
241
|
+
const rowMenuItems = (rowData: any) => {
|
|
242
|
+
const status = rowData?.status;
|
|
243
|
+
const finalized = rowData?.finalized === true;
|
|
244
|
+
const isOpen = status === 'in_progress' || status === 'paused';
|
|
245
|
+
const items: any[] = [];
|
|
246
|
+
|
|
247
|
+
if (isOpen) {
|
|
248
|
+
items.push({
|
|
249
|
+
label: 'Complete Test',
|
|
250
|
+
icon: 'pi pi-check-circle',
|
|
251
|
+
command: () => handleLifecycle(rowData, 'complete'),
|
|
252
|
+
});
|
|
253
|
+
items.push({
|
|
254
|
+
label: 'Abandon Test',
|
|
255
|
+
icon: 'pi pi-ban',
|
|
256
|
+
command: () => confirmDialog({
|
|
257
|
+
header: 'Abandon Test',
|
|
258
|
+
message:
|
|
259
|
+
`Mark ${rowData?.sample_id || rowData?.run_id} as abandoned? ` +
|
|
260
|
+
`Recorded data is kept, but the test can no longer be reopened.`,
|
|
261
|
+
icon: 'pi pi-exclamation-triangle',
|
|
262
|
+
acceptClassName: 'p-button-danger',
|
|
263
|
+
acceptLabel: 'Abandon',
|
|
264
|
+
accept: () => handleLifecycle(rowData, 'abandon'),
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
} else if (status === 'completed' && !finalized) {
|
|
268
|
+
items.push({
|
|
269
|
+
label: 'Reopen Test',
|
|
270
|
+
icon: 'pi pi-replay',
|
|
271
|
+
command: () => confirmDialog({
|
|
272
|
+
header: 'Reopen Test',
|
|
273
|
+
message:
|
|
274
|
+
`Reopen ${rowData?.sample_id || rowData?.run_id} so new cycles are added to ` +
|
|
275
|
+
`it? Cycle numbering continues from where it stopped. This changes where ` +
|
|
276
|
+
`data is recorded — it does not start the machine.`,
|
|
277
|
+
icon: 'pi pi-replay',
|
|
278
|
+
acceptLabel: 'Reopen',
|
|
279
|
+
accept: () => handleLifecycle(rowData, 'reopen'),
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
items.push({
|
|
284
|
+
label: finalized ? 'Abandoned — cannot reopen' : 'No actions available',
|
|
285
|
+
icon: 'pi pi-info-circle',
|
|
286
|
+
disabled: true,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return items;
|
|
290
|
+
};
|
|
291
|
+
|
|
179
292
|
const handleToggleExclude = async (rowData: any) => {
|
|
180
293
|
const runId = rowData?.run_id;
|
|
181
294
|
const rowMethodId = rowData?.method_id ?? methodId;
|
|
@@ -440,7 +553,34 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
440
553
|
);
|
|
441
554
|
}}
|
|
442
555
|
/>
|
|
556
|
+
{/* Thin per-row lifecycle menu. Kept narrow and iconic so it
|
|
557
|
+
doesn't compete with the download actions. */}
|
|
558
|
+
<Column
|
|
559
|
+
header=""
|
|
560
|
+
style={{ width: '3rem' }}
|
|
561
|
+
body={(rowData) => (
|
|
562
|
+
<Button
|
|
563
|
+
icon={
|
|
564
|
+
lifecycleBusy === rowData.run_id
|
|
565
|
+
? 'pi pi-spin pi-spinner'
|
|
566
|
+
: 'pi pi-ellipsis-v'
|
|
567
|
+
}
|
|
568
|
+
text
|
|
569
|
+
rounded
|
|
570
|
+
size="small"
|
|
571
|
+
disabled={lifecycleBusy !== null}
|
|
572
|
+
aria-label="Test actions"
|
|
573
|
+
tooltip="Complete / abandon / reopen this test"
|
|
574
|
+
tooltipOptions={{ position: 'left' }}
|
|
575
|
+
onClick={(e) => {
|
|
576
|
+
setMenuRow(rowData);
|
|
577
|
+
rowMenuRef.current?.toggle(e);
|
|
578
|
+
}}
|
|
579
|
+
/>
|
|
580
|
+
)}
|
|
581
|
+
/>
|
|
443
582
|
</DataTable>
|
|
583
|
+
<Menu model={menuRow ? rowMenuItems(menuRow) : []} popup ref={rowMenuRef} />
|
|
444
584
|
</div>
|
|
445
585
|
);
|
|
446
586
|
};
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import React from 'react';
|
|
17
17
|
import { DataTable } from 'primereact/datatable';
|
|
18
18
|
import { Column } from 'primereact/column';
|
|
19
|
+
import { InputText } from 'primereact/inputtext';
|
|
19
20
|
import type { ColumnBodyOptions } from 'primereact/column';
|
|
20
21
|
import './ScienceTable.css';
|
|
21
22
|
|
|
@@ -32,6 +33,14 @@ export interface ScienceColumn {
|
|
|
32
33
|
body?: (row: any) => React.ReactNode;
|
|
33
34
|
/** Optional per-column style (e.g. minWidth). */
|
|
34
35
|
style?: React.CSSProperties;
|
|
36
|
+
/**
|
|
37
|
+
* Operator-editable cell. Requires the table's `onCellEdit`. Intended for
|
|
38
|
+
* ANNOTATION columns only (a specimen or lot identifier noted against an
|
|
39
|
+
* automatically-captured row) — never for measured values, which the server
|
|
40
|
+
* refuses to rewrite anyway (`tis.update_cycle` only accepts cycle_fields
|
|
41
|
+
* declared `editable: true`).
|
|
42
|
+
*/
|
|
43
|
+
editable?: boolean;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export interface ScienceTableProps {
|
|
@@ -49,6 +58,13 @@ export interface ScienceTableProps {
|
|
|
49
58
|
scrollHeight?: string;
|
|
50
59
|
/** Virtual-scroll row height in px. Default 38. */
|
|
51
60
|
virtualItemSize?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Commit handler for cells in columns marked `editable`. Called with the
|
|
63
|
+
* edited row, the column field and the new value. Without it, `editable`
|
|
64
|
+
* columns stay read-only — there is no local-only edit mode, because a value
|
|
65
|
+
* that looks saved but isn't is worse than one that can't be typed.
|
|
66
|
+
*/
|
|
67
|
+
onCellEdit?: (row: any, field: string, value: any) => void | Promise<void>;
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
const HeaderCell: React.FC<{ column: ScienceColumn }> = ({ column }) => (
|
|
@@ -68,8 +84,12 @@ export const ScienceTable: React.FC<ScienceTableProps> = ({
|
|
|
68
84
|
scrollable = false,
|
|
69
85
|
scrollHeight,
|
|
70
86
|
virtualItemSize = 38,
|
|
87
|
+
onCellEdit,
|
|
71
88
|
}) => {
|
|
72
89
|
const virtual = scrollable && !!scrollHeight;
|
|
90
|
+
// Cell edit mode is switched on only when a column asks for it AND a commit
|
|
91
|
+
// handler exists, so a plain report table keeps DataTable's read-only path.
|
|
92
|
+
const anyEditable = !!onCellEdit && columns.some((c) => c.editable);
|
|
73
93
|
return (
|
|
74
94
|
<div className="ac-science-table-wrap">
|
|
75
95
|
{title != null && <div className="ac-science-table-title">{title}</div>}
|
|
@@ -81,6 +101,7 @@ export const ScienceTable: React.FC<ScienceTableProps> = ({
|
|
|
81
101
|
scrollHeight={scrollHeight}
|
|
82
102
|
virtualScrollerOptions={virtual ? { itemSize: virtualItemSize } : undefined}
|
|
83
103
|
emptyMessage={emptyMessage}
|
|
104
|
+
editMode={anyEditable ? 'cell' : undefined}
|
|
84
105
|
>
|
|
85
106
|
{showIndex ? (
|
|
86
107
|
<Column
|
|
@@ -93,18 +114,56 @@ export const ScienceTable: React.FC<ScienceTableProps> = ({
|
|
|
93
114
|
body={(_data: any, options: ColumnBodyOptions) => options.rowIndex + 1}
|
|
94
115
|
/>
|
|
95
116
|
) : null}
|
|
96
|
-
{columns.map((c) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
{columns.map((c) => {
|
|
118
|
+
const cellEditable = !!onCellEdit && !!c.editable;
|
|
119
|
+
return (
|
|
120
|
+
<Column
|
|
121
|
+
key={c.field}
|
|
122
|
+
field={c.field}
|
|
123
|
+
header={<HeaderCell column={c} />}
|
|
124
|
+
align={c.align ?? 'center'}
|
|
125
|
+
alignHeader="center"
|
|
126
|
+
style={c.style}
|
|
127
|
+
bodyClassName={
|
|
128
|
+
cellEditable ? 'ac-sci-cell ac-sci-cell-editable' : 'ac-sci-cell'
|
|
129
|
+
}
|
|
130
|
+
body={c.body ? (row: any) => c.body!(row) : undefined}
|
|
131
|
+
editor={
|
|
132
|
+
cellEditable
|
|
133
|
+
? (options: any) => (
|
|
134
|
+
<InputText
|
|
135
|
+
value={options.value ?? ''}
|
|
136
|
+
autoFocus
|
|
137
|
+
style={{ width: '100%' }}
|
|
138
|
+
onChange={(e) => options.editorCallback?.(e.target.value)}
|
|
139
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
140
|
+
/>
|
|
141
|
+
)
|
|
142
|
+
: undefined
|
|
143
|
+
}
|
|
144
|
+
onCellEditComplete={
|
|
145
|
+
cellEditable
|
|
146
|
+
? (e: any) => {
|
|
147
|
+
// Numeric annotation columns come back
|
|
148
|
+
// as a string from the text editor;
|
|
149
|
+
// coerce so the stored type matches the
|
|
150
|
+
// schema instead of silently turning a
|
|
151
|
+
// number column into strings.
|
|
152
|
+
const prev = e.rowData?.[c.field];
|
|
153
|
+
let next: any = e.newValue;
|
|
154
|
+
if (typeof prev === 'number' && next !== '' && next != null) {
|
|
155
|
+
const n = Number(next);
|
|
156
|
+
if (!Number.isNaN(n)) next = n;
|
|
157
|
+
}
|
|
158
|
+
if (next !== prev) {
|
|
159
|
+
void onCellEdit!(e.rowData, c.field, next);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
: undefined
|
|
163
|
+
}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
108
167
|
</DataTable>
|
|
109
168
|
</div>
|
|
110
169
|
</div>
|
|
@@ -72,6 +72,12 @@ export interface TestFieldDef {
|
|
|
72
72
|
* (label === stored value) or an explicit { label, value } pair.
|
|
73
73
|
* Mirrors the Rust TestField::options (untagged FieldOption). */
|
|
74
74
|
options?: Array<string | number | boolean | { label: string; value: any }>;
|
|
75
|
+
/** Operator-editable after recording — annotation columns only (a specimen
|
|
76
|
+
* or lot id noted against a captured row). Meaningful on `cycle_fields`;
|
|
77
|
+
* the server refuses `tis.update_cycle` for any field without it, so
|
|
78
|
+
* measured columns can never be typed over. Mirrors Rust
|
|
79
|
+
* `TestField::editable`. */
|
|
80
|
+
editable?: boolean;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
export interface ChartAxis { field?: string; column?: string; label?: string; }
|
|
@@ -155,10 +161,16 @@ export interface TestConfiguration {
|
|
|
155
161
|
defaults?: { [field: string]: any };
|
|
156
162
|
}
|
|
157
163
|
export interface TestMethod {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
/* All four field lists are OPTIONAL by design. Server-side each is
|
|
165
|
+
* `#[serde(default)]`, so a method that declares only `config_fields` is
|
|
166
|
+
* perfectly valid and the other keys simply aren't in the JSON. Typing
|
|
167
|
+
* them as required let `schema.cycle_fields.map(...)` compile and then
|
|
168
|
+
* blank the operator's page at runtime (gen4_ac, 2026-08-11). Keep them
|
|
169
|
+
* optional so the compiler demands a `?? []` at every use. */
|
|
170
|
+
project_fields?: TestFieldDef[];
|
|
171
|
+
config_fields?: TestFieldDef[];
|
|
172
|
+
cycle_fields?: TestFieldDef[];
|
|
173
|
+
results_fields?: TestFieldDef[];
|
|
162
174
|
raw_data?: RawDataShape | null;
|
|
163
175
|
views?: { [name: string]: ChartView };
|
|
164
176
|
/** Optional pretty label for the Test Method picker. */
|
|
@@ -208,6 +220,46 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
208
220
|
const [rawOpen, setRawOpen] = useState(false);
|
|
209
221
|
const [configOpen, setConfigOpen] = useState(false);
|
|
210
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Commit an operator edit to an annotation cell (`tis.update_cycle`).
|
|
225
|
+
*
|
|
226
|
+
* The server is the gate: it accepts a field only if the method declares it
|
|
227
|
+
* `editable: true`, so a measured column is refused even if a caller asks.
|
|
228
|
+
* We update local state only after the round trip succeeds — showing an edit
|
|
229
|
+
* that didn't persist is worse than showing the old value.
|
|
230
|
+
*/
|
|
231
|
+
const saveCycleField = useCallback(
|
|
232
|
+
async (row: any, field: string, value: any) => {
|
|
233
|
+
const cycleIndex = row?.cycle_index;
|
|
234
|
+
if (!projectId || !methodId || !runId || !cycleIndex) return;
|
|
235
|
+
try {
|
|
236
|
+
const resp: any = await invoke(
|
|
237
|
+
'tis.update_cycle' as any,
|
|
238
|
+
MessageType.Request,
|
|
239
|
+
{
|
|
240
|
+
project_id: projectId,
|
|
241
|
+
method_id: methodId,
|
|
242
|
+
run_id: runId,
|
|
243
|
+
cycle_index: cycleIndex,
|
|
244
|
+
fields: { [field]: value },
|
|
245
|
+
} as any,
|
|
246
|
+
);
|
|
247
|
+
if (resp?.success === false) {
|
|
248
|
+
console.error('tis.update_cycle rejected:', resp?.error_message);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
setCycles((prev) =>
|
|
252
|
+
prev.map((c) =>
|
|
253
|
+
c?.cycle_index === cycleIndex ? { ...c, [field]: value } : c,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.error('tis.update_cycle failed:', e);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
[invoke, projectId, methodId, runId],
|
|
261
|
+
);
|
|
262
|
+
|
|
211
263
|
// Direct handle on the chart.js instance so the toolbar's reset-
|
|
212
264
|
// zoom button can call chart.resetZoom() — the zoom plugin's only
|
|
213
265
|
// imperative API. Customers like the wheel/pinch/drag zoom but
|
|
@@ -742,10 +794,16 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
742
794
|
doesn't render 1000 row elements at once. */}
|
|
743
795
|
{(() => {
|
|
744
796
|
const useVirtual = cycles.length > CYCLE_VIRTUAL_THRESHOLD;
|
|
745
|
-
|
|
797
|
+
// `?? []` is load-bearing: cycle_fields is `#[serde(default)]`
|
|
798
|
+
// server-side, so a method that declares only config_fields is
|
|
799
|
+
// valid and arrives with the key absent. Without the guard the
|
|
800
|
+
// `.map` throws inside render and React unmounts the whole
|
|
801
|
+
// app — the operator sees a blank page, not a missing table.
|
|
802
|
+
const cycleColumns: ScienceColumn[] = (schema.cycle_fields ?? []).map(f => ({
|
|
746
803
|
field: f.name,
|
|
747
804
|
label: f.name,
|
|
748
805
|
units: f.units,
|
|
806
|
+
editable: f.editable,
|
|
749
807
|
body: (row) => formatCell(row[f.name], f.type, f.scale),
|
|
750
808
|
}));
|
|
751
809
|
return (
|
|
@@ -756,6 +814,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
756
814
|
scrollable={useVirtual}
|
|
757
815
|
scrollHeight={useVirtual ? cycleTableHeight : undefined}
|
|
758
816
|
emptyMessage="No cycles yet."
|
|
817
|
+
onCellEdit={saveCycleField}
|
|
759
818
|
/>
|
|
760
819
|
);
|
|
761
820
|
})()}
|
|
@@ -767,7 +826,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
767
826
|
{(() => {
|
|
768
827
|
const values = { ...results, ...sampleSummary };
|
|
769
828
|
const hasValues = Object.keys(values).length > 0;
|
|
770
|
-
const resultColumns: ScienceColumn[] = schema.results_fields.map(f => ({
|
|
829
|
+
const resultColumns: ScienceColumn[] = (schema.results_fields ?? []).map(f => ({
|
|
771
830
|
field: f.name,
|
|
772
831
|
label: f.name,
|
|
773
832
|
units: f.units,
|