@adcops/autocore-react 3.6.6 → 3.6.7

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.
@@ -1 +1 @@
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,CA8lBhE,CAAC"}
1
+ {"version":3,"file":"ResultHistoryTable.d.ts","sourceRoot":"","sources":["../../../src/components/tis/ResultHistoryTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkD,MAAM,OAAO,CAAC;AAYvE,OAAO,0BAA0B,CAAC;AAElC,MAAM,WAAW,uBAAuB;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAkED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CA+pBhE,CAAC"}
@@ -1 +1 @@
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),b=useRef(0),[g,y]=useState(null),{invoke:x}=useContext(EventEmitterContext),w=async()=>{if(o){n(!0);try{const e={project_id:o};i&&(e.method_id=i);const t=await x("tis.list_tests",MessageType.Request,e);t.success&&t.data&&t.data.tests&&r(t.data.tests)}catch(e){}n(!1)}else r([])};useEffect(()=>{w()},[o,i,t.state.activeRunId,t.state.active]),useEffect(()=>{_&&f.current?.show({currentTarget:_.anchor})},[_]);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 x(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)}},j=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";y(s);try{const e=await x(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 w()}catch(e){alert(`${t} failed: ${e instanceof Error?e.message:String(e)}`)}finally{y(null)}},C=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 x(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)}},T=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:()=>C("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:()=>C("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:w,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:T,sortFunction:e=>{const t=[...e.data];return t.sort((t,o)=>T(t).localeCompare(T(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";if("completed"===t&&!0===e?.auto_closed)return{label:"Interrupted",severity:"warning"};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 x("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 w()}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:g===e.run_id?"pi pi-spin pi-spinner":"pi pi-ellipsis-v",text:!0,rounded:!0,size:"small",disabled:null!==g,"aria-label":"Test actions",tooltip:"Complete / abandon / reopen this test",tooltipOptions:{position:"left"},onClick:t=>{b.current+=1,h({row:e,anchor:t.currentTarget,seq:b.current})}})})]}),_jsx(Menu,{model:_?(e=>{const t=e?.status,o=!0===e?.finalized,i="in_progress"===t||"paused"===t,s="completed"===t&&!0===e?.auto_closed&&!o,r=[];return i?(r.push({label:"Complete Test",icon:"pi pi-check-circle",command:()=>j(e,"complete")}),r.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:()=>j(e,"abandon")})})):s?(r.push({label:"Close Test",icon:"pi pi-check-circle",command:()=>confirmDialog({header:"Close Interrupted Test",message:`Close ${e?.sample_id||e?.run_id}? It was closed automatically when the software restarted mid-test. Closing it marks it Completed and keeps the recorded end time. Recorded data is not changed, and the test can still be reopened later.`,icon:"pi pi-check-circle",acceptLabel:"Close Test",accept:()=>j(e,"complete")})}),r.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:()=>j(e,"reopen")})}),r.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:()=>j(e,"abandon")})})):"completed"!==t||o?r.push({label:o?"Abandoned — cannot reopen":"No actions available",icon:"pi pi-info-circle",disabled:!0}):r.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:()=>j(e,"reopen")})}),r})(_.row):[],popup:!0,ref:f})]})};
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{Toast}from"primereact/toast";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()},LIFECYCLE_DONE={complete:{summary:"Test closed",detail:e=>`${e} is now Completed. It can still be reopened.`},abandon:{summary:"Test abandoned",detail:e=>`${e} is out of the history and out of the sample averages. Its data is still on disk — use Show abandoned to see it.`},reopen:{summary:"Test reopened",detail:e=>`${e} is open again — new cycles are added to it.`}};export const ResultHistoryTable=e=>{const t=useTis(),o=e.projectId??t.selection.projectId,i=e.methodId,[s,a]=useState([]),[r,n]=useState(!1),[l,d]=useState(null),[c,p]=useState(null),[u,m]=useState(null),f=useRef(null),[h,_]=useState(null),b=useRef(0),[y,g]=useState(null),[w,x]=useState(!1),v=useRef(null),{invoke:C}=useContext(EventEmitterContext),j=useRef(0),T=async()=>{if(!o)return void a([]);const e=++j.current;n(!0);try{const t={project_id:o};i&&(t.method_id=i);const s=await C("tis.list_tests",MessageType.Request,t);if(e!==j.current)return;s.success&&s.data&&s.data.tests&&a(s.data.tests)}catch(e){}e===j.current&&n(!1)};useEffect(()=>{T()},[o,i,t.state.activeRunId,t.state.active]),useEffect(()=>{h&&f.current?.show({currentTarget:h.anchor})},[h]);const $=async(e,t)=>{const s=e?.run_id,a=e?.method_id??i;if(!s||!a||!o)return;const r="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 C(r,MessageType.Request,{project_id:o,method_id:a,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}_${a}_${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)}},R=async(e,t)=>{const s=e?.run_id,a=e?.method_id??i;if(!s||!a||!o)return;const r="complete"===t?"tis.finish_test":"abandon"===t?"tis.reset_test":"tis.resume_test",n=e?.sample_id||s;g(s);try{const e=await C(r,MessageType.Request,{project_id:o,method_id:a,run_id:s});if(!e?.success)return void alert(`Could not ${t} ${s}`+(e?.error_message?`:\n\n${e.error_message}`:""));await T(),v.current?.show({severity:"abandon"===t?"warn":"success",summary:LIFECYCLE_DONE[t].summary,detail:LIFECYCLE_DONE[t].detail(n),life:4e3})}catch(e){alert(`${t} failed: ${e instanceof Error?e.message:String(e)}`)}finally{g(null)}},S=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 C(t,MessageType.Request,{project_id:o});if(!s?.success)return void alert(`Failed to build ${i}`+(s?.error_message?`: ${s.error_message}`:""));const a="string"==typeof s.data?.download_url?s.data.download_url:"";if(!a)return void alert(`Server did not return a download URL for ${i}.`);const r="report"===e?`${o}_project_report.csv`:`${o}_project_archive.zip`,n="string"==typeof s.data?.filename&&s.data.filename?s.data.filename:r;downloadViaUrl(a,n)}catch(e){alert(`Download failed: ${e instanceof Error?e.message:String(e)}`)}finally{p(null)}},k=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:""},I=s.filter(e=>!0===e?.finalized).length,E=w?s:s.filter(e=>!0!==e?.finalized);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:[I>0&&_jsx(Button,{icon:w?"pi pi-eye-slash":"pi pi-eye",label:w?"Hide abandoned":`Show abandoned (${I})`,size:"small",outlined:!0,severity:"secondary",onClick:()=>x(e=>!e)}),_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:()=>S("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:()=>S("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:T,disabled:r})]})]}),_jsx(SampleSummaryPanel,{projectId:o,methodId:i}),_jsxs(DataTable,{value:E,loading:r,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:k,sortFunction:e=>{const t=[...e.data];return t.sort((t,o)=>k(t).localeCompare(k(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";if("completed"===t&&!0===e?.auto_closed)return{label:"Interrupted",severity:"warning"};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 a=!0!==e?.excluded;m(t);try{const e=await C("tis.set_test_excluded",MessageType.Request,{project_id:o,method_id:s,run_id:t,excluded:a});if(!e?.success)return void alert(`Failed to ${a?"exclude":"include"} ${t}`+(e?.error_message?`: ${e.error_message}`:""));await T()}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:()=>$(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:()=>$(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=>{b.current+=1,_({row:e,anchor:t.currentTarget,seq:b.current})}})})]}),_jsx(Menu,{model:h?(e=>{const t=e?.status,o=!0===e?.finalized,i="in_progress"===t||"paused"===t,s="completed"===t&&!0===e?.auto_closed&&!o,a=[];return i?(a.push({label:"Complete Test",icon:"pi pi-check-circle",command:()=>R(e,"complete")}),a.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:()=>R(e,"abandon")})})):s?(a.push({label:"Close Test",icon:"pi pi-check-circle",command:()=>confirmDialog({header:"Close Interrupted Test",message:`Close ${e?.sample_id||e?.run_id}? It was closed automatically when the software restarted mid-test. Closing it marks it Completed and keeps the recorded end time. Recorded data is not changed, and the test can still be reopened later.`,icon:"pi pi-check-circle",acceptLabel:"Close Test",accept:()=>R(e,"complete")})}),a.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:()=>R(e,"reopen")})}),a.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:()=>R(e,"abandon")})})):"completed"!==t||o?a.push({label:o?"Abandoned — cannot reopen":"No actions available",icon:"pi pi-info-circle",disabled:!0}):a.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:()=>R(e,"reopen")})}),a})(h.row):[],popup:!0,ref:f}),_jsx(Toast,{ref:v,position:"top-right"})]})};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.6.6",
3
+ "version": "3.6.7",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -4,6 +4,7 @@ import { Column } from 'primereact/column';
4
4
  import { Button } from 'primereact/button';
5
5
  import { Badge } from 'primereact/badge';
6
6
  import { Menu } from 'primereact/menu';
7
+ import { Toast } from 'primereact/toast';
7
8
  import { confirmDialog } from 'primereact/confirmdialog';
8
9
  import { EventEmitterContext } from '../../core/EventEmitterContext';
9
10
  import { MessageType } from '../../hub/CommandMessage';
@@ -63,6 +64,31 @@ const downloadViaUrl = (url: string, filename: string) => {
63
64
  a.remove();
64
65
  };
65
66
 
67
+ /**
68
+ * What each lifecycle action tells the operator once the server has agreed.
69
+ * A table rather than a switch so the wording for an action lives in exactly
70
+ * one place, next to the other two it has to read consistently with.
71
+ */
72
+ const LIFECYCLE_DONE: Record<
73
+ 'complete' | 'abandon' | 'reopen',
74
+ { summary: string; detail: (label: string) => string }
75
+ > = {
76
+ complete: {
77
+ summary: 'Test closed',
78
+ detail: (l) => `${l} is now Completed. It can still be reopened.`,
79
+ },
80
+ abandon: {
81
+ summary: 'Test abandoned',
82
+ detail: (l) =>
83
+ `${l} is out of the history and out of the sample averages. ` +
84
+ `Its data is still on disk — use Show abandoned to see it.`,
85
+ },
86
+ reopen: {
87
+ summary: 'Test reopened',
88
+ detail: (l) => `${l} is open again — new cycles are added to it.`,
89
+ },
90
+ };
91
+
66
92
  type DownloadKind = 'data' | 'report';
67
93
  type InFlight = { runId: string; kind: DownloadKind };
68
94
  type ProjectDownloadKind = 'report' | 'archive';
@@ -92,10 +118,23 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
92
118
  useState<{ row: any; anchor: HTMLElement; seq: number } | null>(null);
93
119
  const menuSeqRef = useRef(0);
94
120
  const [lifecycleBusy, setLifecycleBusy] = useState<string | null>(null);
121
+ // Abandoned runs are hidden by default — see `visibleTests`. The toggle
122
+ // is how they stay auditable without cluttering the operator's list.
123
+ const [showAbandoned, setShowAbandoned] = useState(false);
124
+ const toastRef = useRef<Toast>(null);
95
125
  const { invoke } = useContext(EventEmitterContext);
96
126
 
127
+ // Guards `setTests` against a stale response. `loadTests` is called both
128
+ // directly (Refresh, and after a lifecycle action) and from an effect that
129
+ // re-fires on every active-run change, so two fetches are routinely in
130
+ // flight at once. Without this the OLDER response can land last and put
131
+ // the row the operator just closed back to "Interrupted" — which reads,
132
+ // correctly, as "the button did nothing".
133
+ const loadSeqRef = useRef(0);
134
+
97
135
  const loadTests = async () => {
98
136
  if (!projectId) { setTests([]); return; }
137
+ const seq = ++loadSeqRef.current;
99
138
  setLoading(true);
100
139
  try {
101
140
  // When the parent omits methodId, ask the server for all runs
@@ -105,13 +144,16 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
105
144
  if (methodId) payload.method_id = methodId;
106
145
 
107
146
  const resp: any = await invoke('tis.list_tests' as any, MessageType.Request, payload as any);
147
+ // A newer fetch has already been issued (or has already landed):
148
+ // this answer is history, and applying it would undo the newer one.
149
+ if (seq !== loadSeqRef.current) return;
108
150
  if (resp.success && resp.data && resp.data.tests) {
109
151
  setTests(resp.data.tests);
110
152
  }
111
153
  } catch (err) {
112
154
  console.error("Failed to load tests", err);
113
155
  }
114
- setLoading(false);
156
+ if (seq === loadSeqRef.current) setLoading(false);
115
157
  };
116
158
 
117
159
  useEffect(() => {
@@ -227,6 +269,7 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
227
269
  action === 'complete' ? 'tis.finish_test'
228
270
  : action === 'abandon' ? 'tis.reset_test'
229
271
  : 'tis.resume_test';
272
+ const label = rowData?.sample_id || runId;
230
273
  setLifecycleBusy(runId);
231
274
  try {
232
275
  const resp: any = await invoke(topic as any, MessageType.Request, {
@@ -241,6 +284,16 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
241
284
  return;
242
285
  }
243
286
  await loadTests();
287
+ // Say what happened. Close and Abandon both leave the run on disk
288
+ // and change only a badge — one of them then hides the row — so
289
+ // without an acknowledgement the operator has no way to tell a
290
+ // successful action from a dead button, and reports the second.
291
+ toastRef.current?.show({
292
+ severity: action === 'abandon' ? 'warn' : 'success',
293
+ summary: LIFECYCLE_DONE[action].summary,
294
+ detail: LIFECYCLE_DONE[action].detail(label),
295
+ life: 4000,
296
+ });
244
297
  } catch (err) {
245
298
  console.error(`Failed to ${action} test`, err);
246
299
  alert(`${action} failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -259,7 +312,8 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
259
312
  * - completed but not finalized → Reopen (add more cycles under the same
260
313
  * Test ID — the multi-specimen case where a set turned out to be bigger)
261
314
  * - finalized (abandoned) → nothing; `finalized` exists precisely to
262
- * stop a deliberately-abandoned run being resurrected.
315
+ * stop a deliberately-abandoned run being resurrected. Only reachable
316
+ * at all with `Show abandoned` on.
263
317
  */
264
318
  const rowMenuItems = (rowData: any) => {
265
319
  const status = rowData?.status;
@@ -442,6 +496,19 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
442
496
  return '';
443
497
  };
444
498
 
499
+ /**
500
+ * Abandoning a run is the operator saying "this one never happened":
501
+ * TIS drops it from the sample aggregates, and it drops out of this list
502
+ * too. Nothing is deleted — `Show abandoned` brings the rows back with an
503
+ * "Abandoned" badge — but the default view is the set of runs that still
504
+ * count for something, which is what an operator scanning the history is
505
+ * actually looking for.
506
+ */
507
+ const abandonedCount = tests.filter((t) => t?.finalized === true).length;
508
+ const visibleTests = showAbandoned
509
+ ? tests
510
+ : tests.filter((t) => t?.finalized !== true);
511
+
445
512
  // Lifecycle status badge. `status`/`finalized` are written by the TIS
446
513
  // servelet (start_test → in_progress, pause_test → paused, finish_test →
447
514
  // completed, reset_test → finalized). Runs recorded before this field
@@ -477,6 +544,23 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
477
544
  : 'Test History (no project selected)'}
478
545
  </h3>
479
546
  <div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center' }}>
547
+ {/* Only offered when there is something to reveal, so the
548
+ ordinary machine never shows a control for a state it
549
+ has never been in. */}
550
+ {abandonedCount > 0 && (
551
+ <Button
552
+ icon={showAbandoned ? 'pi pi-eye-slash' : 'pi pi-eye'}
553
+ label={
554
+ showAbandoned
555
+ ? 'Hide abandoned'
556
+ : `Show abandoned (${abandonedCount})`
557
+ }
558
+ size="small"
559
+ outlined
560
+ severity="secondary"
561
+ onClick={() => setShowAbandoned((v) => !v)}
562
+ />
563
+ )}
480
564
  <Button
481
565
  icon={projectBusy === 'report' ? 'pi pi-spin pi-spinner' : 'pi pi-file'}
482
566
  label="Download Report"
@@ -513,7 +597,7 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
513
597
  <SampleSummaryPanel projectId={projectId} methodId={methodId} />
514
598
 
515
599
  <DataTable
516
- value={tests}
600
+ value={visibleTests}
517
601
  loading={loading}
518
602
  paginator
519
603
  rows={10}
@@ -671,6 +755,13 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
671
755
  popup
672
756
  ref={rowMenuRef}
673
757
  />
758
+ {/* Owned by this component rather than the host app: the library
759
+ cannot assume a consumer has a global message host, and the
760
+ acknowledgement has to appear wherever the table is used.
761
+ Top-right, not bottom: on a machine HMI the bottom strip is the
762
+ control bar, and a message that covers Start/Stop for four
763
+ seconds is worse than no message at all. */}
764
+ <Toast ref={toastRef} position="top-right" />
674
765
  </div>
675
766
  );
676
767
  };