@adcops/autocore-react 3.3.32 → 3.3.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ export interface TestFieldDef {
3
+ name: string;
4
+ type: string;
5
+ units?: string;
6
+ required?: boolean;
7
+ source?: string;
8
+ }
9
+ export interface ChartAxis {
10
+ field?: string;
11
+ column?: string;
12
+ label?: string;
13
+ }
14
+ export interface ChartSeries {
15
+ field?: string;
16
+ column?: string;
17
+ label?: string;
18
+ y_axis?: 'left' | 'right';
19
+ }
20
+ export interface ChartView {
21
+ title?: string;
22
+ type: 'cycle_scatter' | 'raw_trace';
23
+ x: ChartAxis;
24
+ y: ChartSeries[];
25
+ }
26
+ export interface RawDataShape {
27
+ blob_name: string;
28
+ columns: string[];
29
+ units?: {
30
+ [col: string]: string;
31
+ };
32
+ }
33
+ export interface TestDefinition {
34
+ project_fields: TestFieldDef[];
35
+ config_fields: TestFieldDef[];
36
+ cycle_fields: TestFieldDef[];
37
+ results_fields: TestFieldDef[];
38
+ raw_data?: RawDataShape | null;
39
+ views?: {
40
+ [name: string]: ChartView;
41
+ };
42
+ }
43
+ export interface TestDataViewProps {
44
+ projectId: string;
45
+ definitionId: string;
46
+ runId: string;
47
+ schema: TestDefinition;
48
+ /** Minimum ms between display updates when broadcasts arrive. Default 100. */
49
+ throttleMs?: number;
50
+ /** Fixed cycle-table scroll height. Default "400px". */
51
+ cycleTableHeight?: string;
52
+ }
53
+ export declare const TestDataView: React.FC<TestDataViewProps>;
54
+ //# sourceMappingURL=TestDataView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestDataView.d.ts","sourceRoot":"","sources":["../../src/components/TestDataView.tsx"],"names":[],"mappings":"AAUA,OAAO,KAA2D,MAAM,OAAO,CAAC;AA4BhF,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IAAI,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAAE;AAChF,MAAM,WAAW,WAAW;IAAG,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAAE;AAC5G,MAAM,WAAW,SAAS;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,eAAe,GAAG,WAAW,CAAC;IACpC,CAAC,EAAE,SAAS,CAAC;IACb,CAAC,EAAE,WAAW,EAAE,CAAC;CACpB;AACD,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACrC;AACD,MAAM,WAAW,cAAc;IAC3B,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,aAAa,EAAI,YAAY,EAAE,CAAC;IAChC,YAAY,EAAK,YAAY,EAAE,CAAC;IAChC,cAAc,EAAG,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAQ,YAAY,GAAG,IAAI,CAAC;IACrC,KAAK,CAAC,EAAW;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;CAClD;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,EAAK,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAS,MAAM,CAAC;IACrB,MAAM,EAAQ,cAAc,CAAC;IAC7B,8EAA8E;IAC9E,UAAU,CAAC,EAAG,MAAM,CAAC;IACrB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAkOpD,CAAC"}
@@ -0,0 +1 @@
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useContext,useEffect,useMemo,useRef,useState}from"react";import{Button}from"primereact/button";import{Column}from"primereact/column";import{DataTable}from"primereact/datatable";import{Dialog}from"primereact/dialog";import{Dropdown}from"primereact/dropdown";import{Chart as ChartJS,CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend}from"chart.js";import zoomPlugin from"chartjs-plugin-zoom";import{Line}from"react-chartjs-2";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";import{TestRawDataView}from"./TestRawDataView";ChartJS.register(CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend,zoomPlugin);export const TestDataView=({projectId:e,definitionId:t,runId:i,schema:r,throttleMs:a=100,cycleTableHeight:s="400px"})=>{const{invoke:l,subscribe:n,unsubscribe:o}=useContext(EventEmitterContext),[c,d]=useState(null),[m,u]=useState([]),[p,f]=useState({}),[x,h]=useState(!1),g=useMemo(()=>{const e=[];for(const[t,i]of Object.entries(r.views??{}))"cycle_scatter"===i.type&&e.push({name:t,view:i});return e},[r]),[y,_]=useState(g.length>0?g[0].name:null),j=useRef([]),v=useRef(null),b=useRef(null),w=()=>{b.current||(b.current=setTimeout(()=>{if(b.current=null,j.current.length>0){const e=j.current;j.current=[],u(t=>[...e.slice().reverse(),...t])}v.current&&(f(v.current),v.current=null)},a))};useEffect(()=>{let r=!1;return(async()=>{try{const a=await l("results.read_test",MessageType.Request,{project_id:e,definition_id:t,run_id:i});!r&&a?.success&&(d(a.data),f(a.data.results??{}));const s=await l("results.read_cycles",MessageType.Request,{project_id:e,definition_id:t,run_id:i,offset:0,limit:200,order:"desc"});!r&&s?.success&&u(s.data.cycles??[])}catch(e){}})(),()=>{r=!0}},[e,t,i,l]),useEffect(()=>{const r=r=>r?.project_id===e&&r?.definition_id===t&&r?.run_id===i,a=n("results.cycle_added",e=>{r(e)&&e.cycle&&(j.current.push(e.cycle),w())}),s=n("results.results_updated",e=>{r(e)&&(v.current=e.results??{},w())});return()=>{o(a),o(s),b.current&&(clearTimeout(b.current),b.current=null)}},[e,t,i,a]);const C=useMemo(()=>{if(!y||0===g.length)return null;const e=g.find(e=>e.name===y)?.view;if(!e)return null;const t=e.x.field,i=[...m].reverse();return{labels:i.map(e=>e[t]),datasets:e.y.map((e,t)=>({label:e.label??e.field,data:i.map(t=>t[e.field]),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(t),backgroundColor:palette(t),tension:.1,pointRadius:2}))}},[m,y,g]),R=g.find(e=>e.name===y)?.view,S=R?.y.some(e=>"right"===e.y_axis)??!1,T=useMemo(()=>({responsive:!0,maintainAspectRatio:!1,scales:{x:{title:{display:!!R?.x.label,text:R?.x.label}},y:{position:"left",title:{display:!0,text:leftAxisLabel(R)}},...S?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:rightAxisLabel(R)}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},mode:"xy"}}}}),[R,S]);return _jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsx(Header,{meta:c,config:c?.config,runId:i,projectId:e,definitionId:t,canViewRaw:!!r.raw_data,onViewRaw:()=>h(!0)}),g.length>0&&_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsxs("div",{className:"flex",style:{gap:"1rem",alignItems:"center",marginBottom:"0.5rem"},children:[_jsx(Dropdown,{value:y,options:g.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>_(e.value),placeholder:"Select a view"}),_jsx("h3",{style:{margin:0},children:R?.title??""})]}),_jsx("div",{style:{height:320},children:C&&_jsx(Line,{data:C,options:T})})]}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsxs("h3",{style:{marginTop:0},children:["Cycle Data (",m.length,")"]}),_jsx(DataTable,{value:m,scrollable:!0,scrollHeight:s,virtualScrollerOptions:{itemSize:38},emptyMessage:"No cycles yet.",children:r.cycle_fields.map(e=>_jsx(Column,{field:e.name,header:e.units?`${e.name} (${e.units})`:e.name,body:t=>formatCell(t[e.name],e.type)},e.name))})]}),_jsxs("div",{className:"p-card",style:{padding:"1rem"},children:[_jsx("h3",{style:{marginTop:0},children:"Results"}),_jsx(ResultsGrid,{schema:r.results_fields,values:p})]}),r.raw_data&&_jsx(Dialog,{visible:x,onHide:()=>h(!1),header:"Raw Data",style:{width:"90vw",height:"80vh"},maximizable:!0,children:_jsx(TestRawDataView,{projectId:e,definitionId:t,runId:i,schema:r})})]})};const Header=({meta:e,config:t,runId:i,projectId:r,definitionId:a,canViewRaw:s,onViewRaw:l})=>_jsxs("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},children:[a," — ",i]}),_jsxs("div",{style:{color:"var(--text-secondary-color)",fontSize:"0.85em"},children:["project: ",r,e?.start_time&&_jsxs(_Fragment,{children:[" · started: ",new Date(e.start_time).toLocaleString()]})]})]}),s&&_jsx(Button,{icon:"pi pi-chart-line",label:"View Raw Data",onClick:l,outlined:!0})]}),t&&Object.keys(t).length>0&&_jsx("div",{style:{marginTop:"0.75rem",display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(220px, 1fr))",gap:"0.25rem 1rem",fontSize:"0.9em"},children:Object.entries(t).map(([e,t])=>_jsxs("div",{children:[_jsxs("strong",{children:[e,":"]})," ",formatCell(t,"string")]},e))})]}),ResultsGrid=({schema:e,values:t})=>t&&0!==Object.keys(t).length?_jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(220px, 1fr))",gap:"0.5rem 1rem"},children:e.map(e=>_jsxs("div",{children:[_jsxs("div",{style:{fontSize:"0.8em",color:"var(--text-secondary-color)"},children:[e.name,e.units?` (${e.units})`:""]}),_jsx("div",{children:formatCell(t[e.name],e.type)})]},e.name))}):_jsx("div",{style:{color:"var(--text-secondary-color)"},children:"No results yet."}),CHART_COLORS=["#4ea8de","#f59e0b","#22c55e","#a855f7","#ef4444","#14b8a6","#eab308","#ec4899"],palette=e=>CHART_COLORS[e%CHART_COLORS.length],leftAxisLabel=e=>e?.y.filter(e=>"right"!==e.y_axis).map(e=>e.label??e.field).join(" / ")??"",rightAxisLabel=e=>e?.y.filter(e=>"right"===e.y_axis).map(e=>e.label??e.field).join(" / ")??"",formatCell=(e,t)=>null==e?"":"f32"===t||"f64"===t?"number"==typeof e?e.toFixed(4):String(e):"object"==typeof e?JSON.stringify(e):String(e);
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import type { TestDefinition } from './TestDataView';
3
+ export interface TestRawDataViewProps {
4
+ projectId: string;
5
+ definitionId: string;
6
+ runId: string;
7
+ schema: TestDefinition;
8
+ /** Override the blob name (default: schema.raw_data.blob_name). */
9
+ blobName?: string;
10
+ /** Fixed chart height. Default "60vh". */
11
+ chartHeight?: string;
12
+ }
13
+ export declare const TestRawDataView: React.FC<TestRawDataViewProps>;
14
+ //# sourceMappingURL=TestRawDataView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestRawDataView.d.ts","sourceRoot":"","sources":["../../src/components/TestRawDataView.tsx"],"names":[],"mappings":"AAYA,OAAO,KAA2D,MAAM,OAAO,CAAC;AAahF,OAAO,KAAK,EAAa,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOhE,MAAM,WAAW,oBAAoB;IACjC,SAAS,EAAK,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAS,MAAM,CAAC;IACrB,MAAM,EAAQ,cAAc,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,EAAK,MAAM,CAAC;IACrB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA2I1D,CAAC"}
@@ -0,0 +1 @@
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext,useEffect,useMemo,useRef,useState}from"react";import{Button}from"primereact/button";import{Dropdown}from"primereact/dropdown";import{Chart as ChartJS,CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend}from"chart.js";import zoomPlugin from"chartjs-plugin-zoom";import{Line}from"react-chartjs-2";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";ChartJS.register(CategoryScale,LinearScale,PointElement,LineElement,Title,Tooltip,Legend,zoomPlugin);export const TestRawDataView=({projectId:e,definitionId:t,runId:a,schema:i,blobName:n,chartHeight:r="60vh"})=>{const{invoke:o}=useContext(EventEmitterContext),[s,l]=useState(null),[m,d]=useState(!0),[c,p]=useState(null),u=useRef(null),x=useMemo(()=>{const e=[];for(const[t,a]of Object.entries(i.views??{}))"raw_trace"===a.type&&e.push({name:t,view:a});return e},[i]),[h,y]=useState(x.length>0?x[0].name:null),f=n??i.raw_data?.blob_name??"trace";useEffect(()=>{let i=!1;return d(!0),p(null),(async()=>{try{const n=await o("results.read_raw",MessageType.Request,{project_id:e,definition_id:t,run_id:a,name:f});if(i)return;n?.success?l(n.data??{}):p(n?.error_message??"Failed to read raw data")}catch(e){i||p(String(e?.message??e))}finally{i||d(!1)}})(),()=>{i=!0}},[e,t,a,f,o]);const g=useMemo(()=>{if(!s||!h)return null;const e=x.find(e=>e.name===h)?.view;if(!e)return null;const t=e.x.column,a=s[t]??[];return{datasets:e.y.map((e,t)=>({label:e.label??e.column,data:(s[e.column]??[]).map((e,t)=>({x:a[t],y:e})),yAxisID:"right"===e.y_axis?"y1":"y",borderColor:palette(t),backgroundColor:palette(t),pointRadius:0,borderWidth:1.5,showLine:!0}))}},[s,h,x]),_=x.find(e=>e.name===h)?.view,v=_?.y.some(e=>"right"===e.y_axis)??!1,b=useMemo(()=>({responsive:!0,maintainAspectRatio:!1,parsing:!1,scales:{x:{type:"linear",title:{display:!!_?.x.label,text:_?.x.label}},y:{position:"left",title:{display:!0,text:axisLabel(_,"left")}},...v?{y1:{position:"right",grid:{drawOnChartArea:!1},title:{display:!0,text:axisLabel(_,"right")}}}:{}},plugins:{legend:{display:!0},zoom:{pan:{enabled:!0,mode:"xy"},zoom:{wheel:{enabled:!0},pinch:{enabled:!0},drag:{enabled:!0,modifierKey:"shift"},mode:"xy"}}}}),[_,v]);return i.raw_data?0===x.length?_jsx(EmptyState,{message:"No raw_trace views declared. Add one to schema.views in project.json."}):_jsxs("div",{className:"vblock",style:{display:"flex",flexDirection:"column",gap:"1rem",height:"100%"},children:[_jsxs("div",{className:"flex",style:{gap:"1rem",alignItems:"center"},children:[_jsx(Dropdown,{value:h,options:x.map(e=>({label:e.view.title??e.name,value:e.name})),onChange:e=>y(e.value),placeholder:"Select a view"}),_jsx("h3",{style:{margin:0},children:_?.title??""}),_jsx("div",{style:{flex:1}}),_jsx(Button,{icon:"pi pi-refresh",label:"Reset Zoom",outlined:!0,onClick:()=>u.current?.resetZoom?.()})]}),_jsxs("div",{style:{flex:1,minHeight:0,height:r,position:"relative"},children:[m&&_jsx(Overlay,{children:"Loading raw data…"}),c&&_jsx(Overlay,{children:c}),g&&!m&&!c&&_jsx(Line,{ref:u,data:g,options:b})]})]}):_jsx(EmptyState,{message:"No raw_data is declared for this test definition."})};const Overlay=({children:e})=>_jsx("div",{style:{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",color:"var(--text-secondary-color)",pointerEvents:"none"},children:e}),EmptyState=({message:e})=>_jsx("div",{style:{padding:"1rem",color:"var(--text-secondary-color)"},children:e}),CHART_COLORS=["#4ea8de","#f59e0b","#22c55e","#a855f7","#ef4444","#14b8a6","#eab308","#ec4899"],palette=e=>CHART_COLORS[e%CHART_COLORS.length],axisLabel=(e,t)=>e?.y.filter(e=>(e.y_axis??"left")===t).map(e=>e.label??e.column).join(" / ")??"";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.32",
3
+ "version": "3.3.34",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -52,9 +52,12 @@
52
52
  "dependencies": {
53
53
  "@monaco-editor/react": "^4.7.0",
54
54
  "@tauri-apps/api": "^2.9.1",
55
+ "chart.js": "^4.5.1",
56
+ "chartjs-plugin-zoom": "^2.2.0",
55
57
  "clsx": "^2.1.1",
56
58
  "numerable": "^0.3.15",
57
59
  "react-blockly": "^8.1.2",
60
+ "react-chartjs-2": "^5.3.0",
58
61
  "react-simple-keyboard": "^3.8.120",
59
62
  "react-transition-group": "^4.4.5",
60
63
  "sass": "^1.92.1",
@@ -0,0 +1,380 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * TestDataView — standardized test-detail view for the Results System.
5
+ * Renders metadata header + cycle-scatter chart + virtual-scroll cycle
6
+ * table + results table, and subscribes to live `results.cycle_added` /
7
+ * `results.results_updated` broadcasts so the display updates as the
8
+ * control program appends cycles.
9
+ */
10
+
11
+ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
12
+ import { Button } from 'primereact/button';
13
+ import { Column } from 'primereact/column';
14
+ import { DataTable } from 'primereact/datatable';
15
+ import { Dialog } from 'primereact/dialog';
16
+ import { Dropdown } from 'primereact/dropdown';
17
+
18
+ import { Chart as ChartJS,
19
+ CategoryScale, LinearScale, PointElement, LineElement,
20
+ Title, Tooltip, Legend,
21
+ } from 'chart.js';
22
+ import zoomPlugin from 'chartjs-plugin-zoom';
23
+ import { Line } from 'react-chartjs-2';
24
+
25
+ import { EventEmitterContext } from '../core/EventEmitterContext';
26
+ import { MessageType } from '../hub/CommandMessage';
27
+ import { TestRawDataView } from './TestRawDataView';
28
+
29
+ ChartJS.register(
30
+ CategoryScale, LinearScale, PointElement, LineElement,
31
+ Title, Tooltip, Legend, zoomPlugin,
32
+ );
33
+
34
+ // -------------------------------------------------------------------------
35
+ // Types (mirror codegen/codegen_results.rs — kept local so the component
36
+ // works without a hard dependency on any specific generated results.ts)
37
+ // -------------------------------------------------------------------------
38
+
39
+ export interface TestFieldDef {
40
+ name: string;
41
+ type: string;
42
+ units?: string;
43
+ required?: boolean;
44
+ source?: string;
45
+ }
46
+
47
+ export interface ChartAxis { field?: string; column?: string; label?: string; }
48
+ export interface ChartSeries { field?: string; column?: string; label?: string; y_axis?: 'left' | 'right'; }
49
+ export interface ChartView {
50
+ title?: string;
51
+ type: 'cycle_scatter' | 'raw_trace';
52
+ x: ChartAxis;
53
+ y: ChartSeries[];
54
+ }
55
+ export interface RawDataShape {
56
+ blob_name: string;
57
+ columns: string[];
58
+ units?: { [col: string]: string };
59
+ }
60
+ export interface TestDefinition {
61
+ project_fields: TestFieldDef[];
62
+ config_fields: TestFieldDef[];
63
+ cycle_fields: TestFieldDef[];
64
+ results_fields: TestFieldDef[];
65
+ raw_data?: RawDataShape | null;
66
+ views?: { [name: string]: ChartView };
67
+ }
68
+
69
+ export interface TestDataViewProps {
70
+ projectId: string;
71
+ definitionId: string;
72
+ runId: string;
73
+ schema: TestDefinition;
74
+ /** Minimum ms between display updates when broadcasts arrive. Default 100. */
75
+ throttleMs?: number;
76
+ /** Fixed cycle-table scroll height. Default "400px". */
77
+ cycleTableHeight?: string;
78
+ }
79
+
80
+ // -------------------------------------------------------------------------
81
+
82
+ export const TestDataView: React.FC<TestDataViewProps> = ({
83
+ projectId, definitionId, runId, schema,
84
+ throttleMs = 100,
85
+ cycleTableHeight = '400px',
86
+ }) => {
87
+ const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
88
+
89
+ const [meta, setMeta] = useState<any>(null);
90
+ const [cycles, setCycles] = useState<any[]>([]);
91
+ const [results, setResults] = useState<any>({});
92
+ const [rawOpen, setRawOpen] = useState(false);
93
+
94
+ // Scatter-capable views only — raw_trace lives in <TestRawDataView>.
95
+ const scatterViews = useMemo(() => {
96
+ const out: { name: string; view: ChartView }[] = [];
97
+ for (const [name, v] of Object.entries(schema.views ?? {})) {
98
+ if (v.type === 'cycle_scatter') out.push({ name, view: v });
99
+ }
100
+ return out;
101
+ }, [schema]);
102
+
103
+ const [selectedView, setSelectedView] = useState<string | null>(
104
+ scatterViews.length > 0 ? scatterViews[0].name : null,
105
+ );
106
+
107
+ // Pending updates coalesced by a throttle window — keeps React
108
+ // re-renders at <= 1 / throttleMs even if cycles stream faster.
109
+ const pendingCycles = useRef<any[]>([]);
110
+ const pendingResults = useRef<any | null>(null);
111
+ const flushTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
112
+
113
+ const scheduleFlush = () => {
114
+ if (flushTimer.current) return;
115
+ flushTimer.current = setTimeout(() => {
116
+ flushTimer.current = null;
117
+ if (pendingCycles.current.length > 0) {
118
+ const batch = pendingCycles.current;
119
+ pendingCycles.current = [];
120
+ setCycles(prev => [...batch.slice().reverse(), ...prev]); // newest-first
121
+ }
122
+ if (pendingResults.current) {
123
+ setResults(pendingResults.current);
124
+ pendingResults.current = null;
125
+ }
126
+ }, throttleMs);
127
+ };
128
+
129
+ // -----------------------------------------------------------------
130
+ // Initial load
131
+ // -----------------------------------------------------------------
132
+ useEffect(() => {
133
+ let cancelled = false;
134
+ (async () => {
135
+ try {
136
+ const testResp: any = await invoke(
137
+ 'results.read_test' as any, MessageType.Request as any,
138
+ { project_id: projectId, definition_id: definitionId, run_id: runId } as any);
139
+ if (!cancelled && testResp?.success) {
140
+ setMeta(testResp.data);
141
+ setResults(testResp.data.results ?? {});
142
+ }
143
+ const cyResp: any = await invoke(
144
+ 'results.read_cycles' as any, MessageType.Request as any,
145
+ { project_id: projectId, definition_id: definitionId, run_id: runId,
146
+ offset: 0, limit: 200, order: 'desc' } as any);
147
+ if (!cancelled && cyResp?.success) {
148
+ setCycles(cyResp.data.cycles ?? []);
149
+ }
150
+ } catch (e) {
151
+ console.error('[TestDataView] initial load failed', e);
152
+ }
153
+ })();
154
+ return () => { cancelled = true; };
155
+ }, [projectId, definitionId, runId, invoke]);
156
+
157
+ // -----------------------------------------------------------------
158
+ // Live broadcasts
159
+ // -----------------------------------------------------------------
160
+ useEffect(() => {
161
+ const matches = (payload: any) =>
162
+ payload?.project_id === projectId
163
+ && payload?.definition_id === definitionId
164
+ && payload?.run_id === runId;
165
+
166
+ const onCycle = (payload: any) => {
167
+ if (!matches(payload) || !payload.cycle) return;
168
+ pendingCycles.current.push(payload.cycle);
169
+ scheduleFlush();
170
+ };
171
+ const onResults = (payload: any) => {
172
+ if (!matches(payload)) return;
173
+ pendingResults.current = payload.results ?? {};
174
+ scheduleFlush();
175
+ };
176
+
177
+ const id1 = subscribe('results.cycle_added', onCycle);
178
+ const id2 = subscribe('results.results_updated', onResults);
179
+ return () => {
180
+ unsubscribe(id1);
181
+ unsubscribe(id2);
182
+ if (flushTimer.current) { clearTimeout(flushTimer.current); flushTimer.current = null; }
183
+ };
184
+ // eslint-disable-next-line react-hooks/exhaustive-deps
185
+ }, [projectId, definitionId, runId, throttleMs]);
186
+
187
+ // -----------------------------------------------------------------
188
+ // Chart data
189
+ // -----------------------------------------------------------------
190
+ const chartData = useMemo(() => {
191
+ if (!selectedView || scatterViews.length === 0) return null;
192
+ const view = scatterViews.find(v => v.name === selectedView)?.view;
193
+ if (!view) return null;
194
+
195
+ const xField = view.x.field!;
196
+ const asc = [...cycles].reverse(); // cycles state is newest-first; charts want oldest-first
197
+ const xs = asc.map(c => c[xField]);
198
+
199
+ const datasets = view.y.map((s, idx) => ({
200
+ label: s.label ?? s.field,
201
+ data: asc.map(c => c[s.field!]),
202
+ yAxisID: s.y_axis === 'right' ? 'y1' : 'y',
203
+ borderColor: palette(idx),
204
+ backgroundColor: palette(idx),
205
+ tension: 0.1,
206
+ pointRadius: 2,
207
+ }));
208
+
209
+ return { labels: xs, datasets };
210
+ }, [cycles, selectedView, scatterViews]);
211
+
212
+ const selectedViewDef = scatterViews.find(v => v.name === selectedView)?.view;
213
+ const usesRightAxis = selectedViewDef?.y.some(s => s.y_axis === 'right') ?? false;
214
+
215
+ const chartOptions = useMemo(() => ({
216
+ responsive: true,
217
+ maintainAspectRatio: false,
218
+ scales: {
219
+ x: { title: { display: !!selectedViewDef?.x.label, text: selectedViewDef?.x.label } },
220
+ y: { position: 'left' as const,
221
+ title: { display: true, text: leftAxisLabel(selectedViewDef) } },
222
+ ...(usesRightAxis ? {
223
+ y1: { position: 'right' as const,
224
+ grid: { drawOnChartArea: false },
225
+ title: { display: true, text: rightAxisLabel(selectedViewDef) } },
226
+ } : {}),
227
+ },
228
+ plugins: {
229
+ legend: { display: true },
230
+ zoom: {
231
+ pan: { enabled: true, mode: 'xy' as const },
232
+ zoom: {
233
+ wheel: { enabled: true },
234
+ pinch: { enabled: true },
235
+ mode: 'xy' as const,
236
+ },
237
+ },
238
+ },
239
+ }), [selectedViewDef, usesRightAxis]);
240
+
241
+ // -----------------------------------------------------------------
242
+ // Render
243
+ // -----------------------------------------------------------------
244
+ return (
245
+ <div className="vblock" style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
246
+ <Header meta={meta} config={meta?.config} runId={runId}
247
+ projectId={projectId} definitionId={definitionId}
248
+ canViewRaw={!!schema.raw_data}
249
+ onViewRaw={() => setRawOpen(true)} />
250
+
251
+ {scatterViews.length > 0 && (
252
+ <div className="p-card" style={{ padding: '1rem' }}>
253
+ <div className="flex" style={{ gap: '1rem', alignItems: 'center', marginBottom: '0.5rem' }}>
254
+ <Dropdown
255
+ value={selectedView}
256
+ options={scatterViews.map(v => ({ label: v.view.title ?? v.name, value: v.name }))}
257
+ onChange={(e) => setSelectedView(e.value)}
258
+ placeholder="Select a view"
259
+ />
260
+ <h3 style={{ margin: 0 }}>{selectedViewDef?.title ?? ''}</h3>
261
+ </div>
262
+ <div style={{ height: 320 }}>
263
+ {chartData && <Line data={chartData} options={chartOptions} />}
264
+ </div>
265
+ </div>
266
+ )}
267
+
268
+ <div className="p-card" style={{ padding: '1rem' }}>
269
+ <h3 style={{ marginTop: 0 }}>Cycle Data ({cycles.length})</h3>
270
+ <DataTable
271
+ value={cycles}
272
+ scrollable
273
+ scrollHeight={cycleTableHeight}
274
+ virtualScrollerOptions={{ itemSize: 38 }}
275
+ emptyMessage="No cycles yet."
276
+ >
277
+ {schema.cycle_fields.map(f => (
278
+ <Column key={f.name} field={f.name}
279
+ header={f.units ? `${f.name} (${f.units})` : f.name}
280
+ body={(row) => formatCell(row[f.name], f.type)} />
281
+ ))}
282
+ </DataTable>
283
+ </div>
284
+
285
+ <div className="p-card" style={{ padding: '1rem' }}>
286
+ <h3 style={{ marginTop: 0 }}>Results</h3>
287
+ <ResultsGrid schema={schema.results_fields} values={results} />
288
+ </div>
289
+
290
+ {schema.raw_data && (
291
+ <Dialog
292
+ visible={rawOpen}
293
+ onHide={() => setRawOpen(false)}
294
+ header="Raw Data"
295
+ style={{ width: '90vw', height: '80vh' }}
296
+ maximizable
297
+ >
298
+ <TestRawDataView
299
+ projectId={projectId}
300
+ definitionId={definitionId}
301
+ runId={runId}
302
+ schema={schema}
303
+ />
304
+ </Dialog>
305
+ )}
306
+ </div>
307
+ );
308
+ };
309
+
310
+ // -------------------------------------------------------------------------
311
+ // Sub-components and helpers
312
+ // -------------------------------------------------------------------------
313
+
314
+ const Header: React.FC<{
315
+ meta: any; config: any; runId: string;
316
+ projectId: string; definitionId: string;
317
+ canViewRaw: boolean; onViewRaw: () => void;
318
+ }> = ({ meta, config, runId, projectId, definitionId, canViewRaw, onViewRaw }) => (
319
+ <div className="p-card" style={{ padding: '1rem' }}>
320
+ <div className="flex" style={{ justifyContent: 'space-between', alignItems: 'flex-start', gap: '1rem' }}>
321
+ <div>
322
+ <h2 style={{ margin: 0 }}>{definitionId} — {runId}</h2>
323
+ <div style={{ color: 'var(--text-secondary-color)', fontSize: '0.85em' }}>
324
+ project: {projectId}
325
+ {meta?.start_time && <> · started: {new Date(meta.start_time).toLocaleString()}</>}
326
+ </div>
327
+ </div>
328
+ {canViewRaw && (
329
+ <Button icon="pi pi-chart-line" label="View Raw Data" onClick={onViewRaw} outlined />
330
+ )}
331
+ </div>
332
+ {config && Object.keys(config).length > 0 && (
333
+ <div style={{ marginTop: '0.75rem', display: 'grid',
334
+ gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
335
+ gap: '0.25rem 1rem', fontSize: '0.9em' }}>
336
+ {Object.entries(config).map(([k, v]) => (
337
+ <div key={k}><strong>{k}:</strong> {formatCell(v, 'string')}</div>
338
+ ))}
339
+ </div>
340
+ )}
341
+ </div>
342
+ );
343
+
344
+ const ResultsGrid: React.FC<{ schema: TestFieldDef[]; values: any }> = ({ schema, values }) => {
345
+ if (!values || Object.keys(values).length === 0) {
346
+ return <div style={{ color: 'var(--text-secondary-color)' }}>No results yet.</div>;
347
+ }
348
+ return (
349
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '0.5rem 1rem' }}>
350
+ {schema.map(f => (
351
+ <div key={f.name}>
352
+ <div style={{ fontSize: '0.8em', color: 'var(--text-secondary-color)' }}>
353
+ {f.name}{f.units ? ` (${f.units})` : ''}
354
+ </div>
355
+ <div>{formatCell(values[f.name], f.type)}</div>
356
+ </div>
357
+ ))}
358
+ </div>
359
+ );
360
+ };
361
+
362
+ const CHART_COLORS = [
363
+ '#4ea8de', '#f59e0b', '#22c55e', '#a855f7',
364
+ '#ef4444', '#14b8a6', '#eab308', '#ec4899',
365
+ ];
366
+ const palette = (i: number) => CHART_COLORS[i % CHART_COLORS.length];
367
+
368
+ const leftAxisLabel = (v?: ChartView) =>
369
+ v?.y.filter(s => s.y_axis !== 'right').map(s => s.label ?? s.field).join(' / ') ?? '';
370
+ const rightAxisLabel = (v?: ChartView) =>
371
+ v?.y.filter(s => s.y_axis === 'right').map(s => s.label ?? s.field).join(' / ') ?? '';
372
+
373
+ const formatCell = (v: any, type: string): string => {
374
+ if (v === null || v === undefined) return '';
375
+ if (type === 'f32' || type === 'f64') {
376
+ return typeof v === 'number' ? v.toFixed(4) : String(v);
377
+ }
378
+ if (typeof v === 'object') return JSON.stringify(v);
379
+ return String(v);
380
+ };
@@ -0,0 +1,208 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * TestRawDataView — raw-trace viewer for the Results System. Lazy-fetches
5
+ * the columnar `raw_data/<blob>.json` for a single test and renders it
6
+ * using any `raw_trace`-type view declared in the test schema. Supports
7
+ * pinch-zoom, wheel-zoom, and drag-pan.
8
+ *
9
+ * Can be used either standalone (e.g. a dedicated route) or from the
10
+ * built-in dialog inside <TestDataView>.
11
+ */
12
+
13
+ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
14
+ import { Button } from 'primereact/button';
15
+ import { Dropdown } from 'primereact/dropdown';
16
+
17
+ import { Chart as ChartJS,
18
+ CategoryScale, LinearScale, PointElement, LineElement,
19
+ Title, Tooltip, Legend,
20
+ } from 'chart.js';
21
+ import zoomPlugin from 'chartjs-plugin-zoom';
22
+ import { Line } from 'react-chartjs-2';
23
+
24
+ import { EventEmitterContext } from '../core/EventEmitterContext';
25
+ import { MessageType } from '../hub/CommandMessage';
26
+ import type { ChartView, TestDefinition } from './TestDataView';
27
+
28
+ ChartJS.register(
29
+ CategoryScale, LinearScale, PointElement, LineElement,
30
+ Title, Tooltip, Legend, zoomPlugin,
31
+ );
32
+
33
+ export interface TestRawDataViewProps {
34
+ projectId: string;
35
+ definitionId: string;
36
+ runId: string;
37
+ schema: TestDefinition;
38
+ /** Override the blob name (default: schema.raw_data.blob_name). */
39
+ blobName?: string;
40
+ /** Fixed chart height. Default "60vh". */
41
+ chartHeight?: string;
42
+ }
43
+
44
+ export const TestRawDataView: React.FC<TestRawDataViewProps> = ({
45
+ projectId, definitionId, runId, schema,
46
+ blobName,
47
+ chartHeight = '60vh',
48
+ }) => {
49
+ const { invoke } = useContext(EventEmitterContext);
50
+
51
+ const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
52
+ const [loading, setLoading] = useState(true);
53
+ const [error, setError] = useState<string | null>(null);
54
+ const chartRef = useRef<any>(null);
55
+
56
+ // raw_trace-capable views only — cycle scatter lives in <TestDataView>.
57
+ const traceViews = useMemo(() => {
58
+ const out: { name: string; view: ChartView }[] = [];
59
+ for (const [name, v] of Object.entries(schema.views ?? {})) {
60
+ if (v.type === 'raw_trace') out.push({ name, view: v });
61
+ }
62
+ return out;
63
+ }, [schema]);
64
+
65
+ const [selectedView, setSelectedView] = useState<string | null>(
66
+ traceViews.length > 0 ? traceViews[0].name : null,
67
+ );
68
+
69
+ const effectiveBlobName = blobName ?? schema.raw_data?.blob_name ?? 'trace';
70
+
71
+ // Lazy fetch — only runs on mount / when identifiers change.
72
+ useEffect(() => {
73
+ let cancelled = false;
74
+ setLoading(true);
75
+ setError(null);
76
+ (async () => {
77
+ try {
78
+ const resp: any = await invoke(
79
+ 'results.read_raw' as any, MessageType.Request as any,
80
+ { project_id: projectId, definition_id: definitionId,
81
+ run_id: runId, name: effectiveBlobName } as any);
82
+ if (cancelled) return;
83
+ if (resp?.success) {
84
+ setRaw(resp.data ?? {});
85
+ } else {
86
+ setError(resp?.error_message ?? 'Failed to read raw data');
87
+ }
88
+ } catch (e: any) {
89
+ if (!cancelled) setError(String(e?.message ?? e));
90
+ } finally {
91
+ if (!cancelled) setLoading(false);
92
+ }
93
+ })();
94
+ return () => { cancelled = true; };
95
+ }, [projectId, definitionId, runId, effectiveBlobName, invoke]);
96
+
97
+ const chartData = useMemo(() => {
98
+ if (!raw || !selectedView) return null;
99
+ const view = traceViews.find(v => v.name === selectedView)?.view;
100
+ if (!view) return null;
101
+
102
+ const xCol = view.x.column!;
103
+ const xs = raw[xCol] ?? [];
104
+
105
+ const datasets = view.y.map((s, idx) => ({
106
+ label: s.label ?? s.column,
107
+ data: (raw[s.column!] ?? []).map((y, i) => ({ x: xs[i], y })),
108
+ yAxisID: s.y_axis === 'right' ? 'y1' : 'y',
109
+ borderColor: palette(idx),
110
+ backgroundColor: palette(idx),
111
+ pointRadius: 0,
112
+ borderWidth: 1.5,
113
+ showLine: true,
114
+ }));
115
+
116
+ return { datasets };
117
+ }, [raw, selectedView, traceViews]);
118
+
119
+ const selectedViewDef = traceViews.find(v => v.name === selectedView)?.view;
120
+ const usesRightAxis = selectedViewDef?.y.some(s => s.y_axis === 'right') ?? false;
121
+
122
+ const chartOptions = useMemo(() => ({
123
+ responsive: true,
124
+ maintainAspectRatio: false,
125
+ parsing: false as const, // raw points are already {x, y}
126
+ scales: {
127
+ x: { type: 'linear' as const,
128
+ title: { display: !!selectedViewDef?.x.label, text: selectedViewDef?.x.label } },
129
+ y: { position: 'left' as const,
130
+ title: { display: true, text: axisLabel(selectedViewDef, 'left') } },
131
+ ...(usesRightAxis ? {
132
+ y1: { position: 'right' as const,
133
+ grid: { drawOnChartArea: false },
134
+ title: { display: true, text: axisLabel(selectedViewDef, 'right') } },
135
+ } : {}),
136
+ },
137
+ plugins: {
138
+ legend: { display: true },
139
+ zoom: {
140
+ pan: { enabled: true, mode: 'xy' as const },
141
+ zoom: {
142
+ wheel: { enabled: true },
143
+ pinch: { enabled: true },
144
+ drag: { enabled: true, modifierKey: 'shift' as const },
145
+ mode: 'xy' as const,
146
+ },
147
+ },
148
+ },
149
+ }), [selectedViewDef, usesRightAxis]);
150
+
151
+ if (!schema.raw_data) {
152
+ return <EmptyState message="No raw_data is declared for this test definition." />;
153
+ }
154
+ if (traceViews.length === 0) {
155
+ return <EmptyState message="No raw_trace views declared. Add one to schema.views in project.json." />;
156
+ }
157
+
158
+ return (
159
+ <div className="vblock" style={{ display: 'flex', flexDirection: 'column', gap: '1rem', height: '100%' }}>
160
+ <div className="flex" style={{ gap: '1rem', alignItems: 'center' }}>
161
+ <Dropdown
162
+ value={selectedView}
163
+ options={traceViews.map(v => ({ label: v.view.title ?? v.name, value: v.name }))}
164
+ onChange={(e) => setSelectedView(e.value)}
165
+ placeholder="Select a view"
166
+ />
167
+ <h3 style={{ margin: 0 }}>{selectedViewDef?.title ?? ''}</h3>
168
+ <div style={{ flex: 1 }} />
169
+ <Button icon="pi pi-refresh" label="Reset Zoom"
170
+ outlined
171
+ onClick={() => chartRef.current?.resetZoom?.()} />
172
+ </div>
173
+
174
+ <div style={{ flex: 1, minHeight: 0, height: chartHeight, position: 'relative' }}>
175
+ {loading && <Overlay>Loading raw data…</Overlay>}
176
+ {error && <Overlay>{error}</Overlay>}
177
+ {chartData && !loading && !error && (
178
+ <Line ref={chartRef} data={chartData} options={chartOptions} />
179
+ )}
180
+ </div>
181
+ </div>
182
+ );
183
+ };
184
+
185
+ // -------------------------------------------------------------------------
186
+ // helpers
187
+ // -------------------------------------------------------------------------
188
+
189
+ const Overlay: React.FC<{ children: React.ReactNode }> = ({ children }) => (
190
+ <div style={{ position: 'absolute', inset: 0, display: 'flex',
191
+ alignItems: 'center', justifyContent: 'center',
192
+ color: 'var(--text-secondary-color)', pointerEvents: 'none' }}>
193
+ {children}
194
+ </div>
195
+ );
196
+
197
+ const EmptyState: React.FC<{ message: string }> = ({ message }) => (
198
+ <div style={{ padding: '1rem', color: 'var(--text-secondary-color)' }}>{message}</div>
199
+ );
200
+
201
+ const CHART_COLORS = [
202
+ '#4ea8de', '#f59e0b', '#22c55e', '#a855f7',
203
+ '#ef4444', '#14b8a6', '#eab308', '#ec4899',
204
+ ];
205
+ const palette = (i: number) => CHART_COLORS[i % CHART_COLORS.length];
206
+
207
+ const axisLabel = (v: ChartView | undefined, side: 'left' | 'right') =>
208
+ v?.y.filter(s => (s.y_axis ?? 'left') === side).map(s => s.label ?? s.column).join(' / ') ?? '';