@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;
|
|
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
|
@@ -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={
|
|
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
|
};
|