@adcops/autocore-react 3.3.32 → 3.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/IndicatorButton.d.ts +10 -0
- package/dist/components/IndicatorButton.d.ts.map +1 -1
- package/dist/components/IndicatorButton.js +1 -1
- package/dist/components/TestDataView.d.ts +54 -0
- package/dist/components/TestDataView.d.ts.map +1 -0
- package/dist/components/TestDataView.js +1 -0
- package/dist/components/TestRawDataView.d.ts +14 -0
- package/dist/components/TestRawDataView.d.ts.map +1 -0
- package/dist/components/TestRawDataView.js +1 -0
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/core/AutoCoreTagTypes.d.ts +8 -0
- package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
- package/dist/hooks/useAutoCoreTag.d.ts +2 -0
- package/dist/hooks/useAutoCoreTag.d.ts.map +1 -1
- package/dist/hooks/useAutoCoreTag.js +1 -1
- package/package.json +4 -1
- package/src/components/IndicatorButton.tsx +29 -6
- package/src/components/TestDataView.tsx +380 -0
- package/src/components/TestRawDataView.tsx +208 -0
- package/src/core/AutoCoreTagContext.tsx +41 -5
- package/src/core/AutoCoreTagTypes.ts +10 -0
- package/src/hooks/useAutoCoreTag.ts +5 -1
|
@@ -43,6 +43,16 @@ export interface IndicatorButtonProps extends Omit<ButtonProps, "value"> {
|
|
|
43
43
|
* cycle on onColor.
|
|
44
44
|
*/
|
|
45
45
|
flash?: boolean;
|
|
46
|
+
/** Callback fired when the button is pressed (mouse down or touch start). */
|
|
47
|
+
onPress?: (e: React.MouseEvent | React.TouchEvent) => void;
|
|
48
|
+
/** Callback fired when the button is released (mouse up, touch end, or mouse leave). */
|
|
49
|
+
onRelease?: (e: React.MouseEvent | React.TouchEvent) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Momentary tag writer. When the button is pressed, this is called with `true`.
|
|
52
|
+
* When the button is released or the mouse leaves, it is called with `false`.
|
|
53
|
+
* Pass the `write` function from `useAutoCoreTag` directly to this prop.
|
|
54
|
+
*/
|
|
55
|
+
momentary?: (value: boolean) => void | Promise<void>;
|
|
46
56
|
}
|
|
47
57
|
export declare const IndicatorButton: React.FC<IndicatorButtonProps>;
|
|
48
58
|
export default IndicatorButton;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndicatorButton.d.ts","sourceRoot":"","sources":["../../src/components/IndicatorButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuD,MAAM,OAAO,CAAC;AAE5E,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAK7D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,OAAO,uBAAuB,CAAC;AAE/B,MAAM,CAAC,OAAO,MAAM,0BAA0B,GAC1C,MAAM,EAAE,GACR,SAAS,EAAE,GACX,SAAS,CAAC;AAEd,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC;IACtE,2GAA2G;IAC3G,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IAElC,mDAAmD;IACnD,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAE1B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,0BAA0B,CAAC;IAErC,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,UAAU,CAAC;IAExB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,kEAAkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"IndicatorButton.d.ts","sourceRoot":"","sources":["../../src/components/IndicatorButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuD,MAAM,OAAO,CAAC;AAE5E,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAK7D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,OAAO,uBAAuB,CAAC;AAE/B,MAAM,CAAC,OAAO,MAAM,0BAA0B,GAC1C,MAAM,EAAE,GACR,SAAS,EAAE,GACX,SAAS,CAAC;AAEd,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC;IACtE,2GAA2G;IAC3G,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IAElC,mDAAmD;IACnD,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,cAAc,CAAC;IAE1B,+CAA+C;IAC/C,OAAO,CAAC,EAAE,0BAA0B,CAAC;IAErC,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,UAAU,CAAC;IAExB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,kEAAkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAE3D,wFAAwF;IACxF,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAE7D;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD;AAKD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAkK1D,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx}from"react/jsx-runtime";import React,{useCallback,useContext,useEffect,useState}from"react";import clsx from"clsx";import{Button}from"primereact/button";import{EventEmitterContext}from"../core/EventEmitterContext";import{IndicatorColor}from"../core/IndicatorColor";export{IndicatorColor};import{ActionMode}from"../core/ActionMode";export{ActionMode};import"./IndicatorButton.css";const toPrimeIcon=o=>o?o.includes("pi ")?o:`pi ${o}`:void 0;export const IndicatorButton=({topic:o,value:t,onColor:e=IndicatorColor.IndicatorGreen,offColor:n="gray",options:r,icon:i,onIcon:c,offIcon:
|
|
1
|
+
import{jsx as _jsx}from"react/jsx-runtime";import React,{useCallback,useContext,useEffect,useState}from"react";import clsx from"clsx";import{Button}from"primereact/button";import{EventEmitterContext}from"../core/EventEmitterContext";import{IndicatorColor}from"../core/IndicatorColor";export{IndicatorColor};import{ActionMode}from"../core/ActionMode";export{ActionMode};import"./IndicatorButton.css";const toPrimeIcon=o=>o?o.includes("pi ")?o:`pi ${o}`:void 0;export const IndicatorButton=({topic:o,value:t,onColor:e=IndicatorColor.IndicatorGreen,offColor:n="gray",options:r,icon:i,onIcon:c,offIcon:s,command:a,commandTopic:l,commandArgs:d,actionMode:u,invert:f,className:m,label:p,style:x,disabled:C,hidden:b,flash:I,onPress:h,onRelease:v,momentary:g,...M})=>{const{subscribe:y,unsubscribe:E,dispatch:A}=useContext(EventEmitterContext),[S,k]=useState(void 0),[B,T]=useState(!1);useEffect(()=>{if(!o||!y)return;const t=y(o,o=>k(o));return()=>{try{E&&void 0!==t&&E(t)}catch{}}},[o,y,E]);const j=void 0!==t?t:S,P=(()=>{if(!r||0===r.length)return p;if(r.length>2){if("number"==typeof j){const o=Math.trunc(j);if(o>=0&&o<r.length&&null!=r[o])return String(r[o])}else if("string"==typeof j){const o=r.findIndex(o=>o===j);if(o>=0)return String(r[o])}return p}const o=j?r[1]:r[0];return null!=o&&String(o).length>0?String(o):p})(),R=toPrimeIcon((j?c:s)??i);let w=j?e:n;void 0===j&&(w=IndicatorColor.IndicatorInvalid);const N=useCallback(o=>{if(!a||!A)return;const t={topic:l,value:f?!o:o,...d??{}};A({topic:a,payload:t})},[a,A,f,d,l]),_=useCallback(o=>{B||C||(T(!0),g&&g(!0),h&&h(o),u!==ActionMode.Tap&&u!==ActionMode.Pressed||N(!0))},[B,C,u,N,h,g]),D=useCallback(o=>{B&&!C&&(T(!1),g&&g(!1),v&&v(o),u===ActionMode.Tap?N(!1):u===ActionMode.Released&&N(!0))},[B,C,u,N,v,g]),G={color:"white",backgroundColor:I?void 0:w,display:b?"none":void 0,...I?{"--flash-on":e,"--flash-off":n}:{},...x??{}};return _jsx(Button,{...M,label:P,icon:R,className:clsx(m,I&&"indicator-button-flash"),style:G,disabled:!!C,onMouseDown:_,onTouchStart:_,onMouseUp:D,onTouchEnd:D,onMouseLeave:D})};export default IndicatorButton;
|
|
@@ -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(" / ")??"";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutoCoreTagContext.d.ts","sourceRoot":"","sources":["../../src/core/AutoCoreTagContext.tsx"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,EAQV,KAAK,SAAS,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EACR,gBAAgB,EAChB,SAAS,EACT,WAAW,EACd,MAAM,oBAAoB,CAAC;AAI5B;;;GAGG;AACH,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE3C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"AutoCoreTagContext.d.ts","sourceRoot":"","sources":["../../src/core/AutoCoreTagContext.tsx"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,EAQV,KAAK,SAAS,EACjB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EACR,gBAAgB,EAChB,SAAS,EACT,WAAW,EACd,MAAM,oBAAoB,CAAC;AAI5B;;;GAGG;AACH,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE3C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kBAAkB,8CAU7B,CAAC;AAsBH;;;;GAIG;AAIH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC;IACvC,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,EAAE,SAAS,SAAS,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB,CAufA,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx}from"react/jsx-runtime";import React,{useRef,createContext,useCallback,useContext,useEffect,useMemo,useState}from"react";import{EventEmitterContext}from"./EventEmitterContext";import{MessageType}from"../hub/CommandMessage";export const AutoCoreTagContext=createContext({values:{},rawValues:{},isLoading:!0,write:async()=>{},tap:async()=>{},scales:{},updateScale:async()=>{}});const sleep=e=>new Promise(t=>setTimeout(t,e));export const AutoCoreTagProvider=({children:e,tags:t,scales:a,eagerRead:s=!0})=>{const n=useRef(!1),c=useMemo(()=>({}),[]),r=a??c,{invoke:o,read:
|
|
1
|
+
import{jsx as _jsx}from"react/jsx-runtime";import React,{useRef,createContext,useCallback,useContext,useEffect,useMemo,useState}from"react";import{EventEmitterContext}from"./EventEmitterContext";import{MessageType}from"../hub/CommandMessage";export const AutoCoreTagContext=createContext({values:{},rawValues:{},isLoading:!0,write:async()=>{},tap:async()=>{},press:async()=>{},release:async()=>{},scales:{},updateScale:async()=>{}});const sleep=e=>new Promise(t=>setTimeout(t,e));export const AutoCoreTagProvider=({children:e,tags:t,scales:a,eagerRead:s=!0})=>{const n=useRef(!1),c=useMemo(()=>({}),[]),r=a??c,{invoke:o,read:l,write:u,serverSubscribe:i,isConnected:f,subscribe:m,unsubscribe:y}=useContext(EventEmitterContext),[b,d]=useState(()=>{const e={};for(const a of t)void 0!==a.initialValue&&(e[a.tagName]=a.initialValue);return e}),[g,p]=useState({}),[v,C]=useState(r),N=useRef(v),h=useRef(b);useEffect(()=>{N.current=v},[v]),useEffect(()=>{h.current=b},[b]);const[T,w]=useState(!0),x=useCallback((e,t)=>{const{valueType:a,scale:s,codec:n}=e;if("number"===a&&"number"==typeof t&&s){const e=N.current[s];return t*(e?.scale??1)}if("json"===a&&n?.fromServer)try{return n.fromServer(t)}catch{}return t},[]),E=useCallback((e,t)=>{const{valueType:a,scale:s,codec:n}=e;if("json"===a&&n?.toServer)try{t=n.toServer(t)}catch{}if("number"===a&&"number"==typeof t&&s){const e=N.current[s];return t/(e?.scale??1)}return t},[]),k=useCallback(e=>{const a=t.filter(t=>t.scale===e);a.length&&p(e=>{const t={...e};for(const e of a){const a=h.current[e.tagName];if("number"!=typeof a)continue;const s=x(e,a);t[e.tagName]!==s&&(t[e.tagName]=s)}return t})},[t,x]),S=useCallback((e,t)=>{d(a=>a[e.tagName]===t?a:{...a,[e.tagName]:t});const a=x(e,t);p(t=>t[e.tagName]===a?t:{...t,[e.tagName]:a})},[x]),M=useCallback(async(e,t,a)=>{const s=a?.concurrency??4,n=a?.minDelayMs??20,c=a?.jitterMs??40;let r=0;const u=Array.from({length:s},()=>(async()=>{for(;;){const a=r++;if(a>=t.length)return;const s=t[a];try{let t=!1;try{await o(s.fqdn,MessageType.Request,{action:"refresh"}),t=!0}catch{}if(!t){const t="GNV"===e.toUpperCase()?{group:"ux"}:{};try{const e=await l(s.fqdn,t);e?.success&&S(s,e.data)}catch(e){}}}catch(e){}finally{const e=Math.floor(Math.random()*c);await sleep(n+e)}}})());await Promise.all(u)},[o,l,S]),j=useCallback(async()=>{for(const[e,t]of Object.entries(r)){if(!t.serverTag)continue;const{domain:a,symbolName:s}=t.serverTag;try{const t=await l(`${a}.${s}`,{group:"ux"});if(t.data&&(t.success??1)&&(t.valid??1)){const a=JSON.parse(t.data);if(a&&"number"==typeof a.scale){const{scale:t,label:s}=a;C(a=>({...a,[e]:{...a[e],scale:t,label:s??a[e]?.label??"---"}})),k(e)}}}catch{}}},[l,r,k]);useEffect(()=>{let e=!0;const a=[],c=async()=>{if(e&&!n.current){n.current=!0;try{await(async()=>{try{await j();for(const n of t){await i(n.tagName,n.fqdn,n.subscriptionOptions);const t=m(n.tagName,t=>{e&&S(n,t)});a.push(t),s&&l(n.fqdn).then(t=>{e&&t?.success&&S(n,t.data)}).catch(e=>{})}}finally{e&&setTimeout(()=>e&&w(!1),100)}})()}finally{e&&setTimeout(()=>e&&w(!1),100)}}};if(f())c();else{const e=m("HUB/connected",()=>{y(e),c()});a.push(e)}return()=>{e=!1,a.forEach(y),n.current=!1}},[m,y,f,o,i,s,t,j,M,S]);const q=useMemo(()=>Object.entries(r).filter(([,e])=>e.serverTag).map(([e,t])=>({scaleName:e,domain:t.serverTag.domain,symbolName:t.serverTag.symbolName})),[r]);useEffect(()=>{let e=!0;const t=[];for(const{scaleName:a,domain:s,symbolName:n}of q){const c=m(`${s}.${n}`,t=>{if(!e)return;const s=t?.value;if(s&&"object"==typeof s&&"number"==typeof s.scale){const{scale:e,label:t}=s;C(s=>({...s,[a]:{...s[a],scale:e,label:t??s[a]?.label??"---"}})),k(a)}});t.push(c)}return()=>{e=!1,t.forEach(y)}},[m,y,q,k]);const R=useCallback(async(e,a)=>{const s=t.find(t=>t.tagName===e);if(!s)return;const n=E(s,a);await u(s.fqdn,n)},[t,u,E]),$=useCallback(async e=>{const a=t.find(t=>t.tagName===e);a&&"boolean"===a.valueType&&(u(a.fqdn,!0).catch(e=>{}),await sleep(300),u(a.fqdn,!1).catch(e=>{}))},[t,u]),V=useCallback(async e=>{const a=t.find(t=>t.tagName===e);a&&"boolean"===a.valueType&&await u(a.fqdn,!0)},[t,u]),A=useCallback(async e=>{const a=t.find(t=>t.tagName===e);a&&"boolean"===a.valueType&&await u(a.fqdn,!1)},[t,u]),O=useCallback(async(e,t,a)=>{const s=v[e];if(s){if(s.serverTag){const n=`${s.serverTag.domain}.${s.serverTag.symbolName}`;await u(n,{name:e,scale:t,label:a})}C(s=>({...s,[e]:{...s[e],scale:t,label:a}})),k(e)}},[v,u,k]);useEffect(()=>{p(e=>{const a={...e};for(const e of t){const t=h.current[e.tagName];if(void 0===t)continue;const s=x(e,t);a[e.tagName]!==s&&(a[e.tagName]=s)}return a})},[t,x,v]);const P=useMemo(()=>({values:g,rawValues:b,isLoading:T,write:R,tap:$,press:V,release:A,scales:v,updateScale:O}),[g,b,T,R,$,V,A,v,O]);return _jsx(AutoCoreTagContext.Provider,{value:P,children:e})};
|
|
@@ -39,6 +39,10 @@ export type TagBinding<T> = Readonly<{
|
|
|
39
39
|
write: (displayValue: T) => Promise<void>;
|
|
40
40
|
/** Momentary action; typically boolean true→false pulse. */
|
|
41
41
|
tap: () => Promise<void>;
|
|
42
|
+
/** Sets boolean tag to true. */
|
|
43
|
+
press: () => Promise<void>;
|
|
44
|
+
/** Sets boolean tag to false. */
|
|
45
|
+
release: () => Promise<void>;
|
|
42
46
|
/** True while initial read/bootstrapping is in progress. */
|
|
43
47
|
isLoading: boolean;
|
|
44
48
|
}>;
|
|
@@ -268,6 +272,10 @@ export interface BaseContextValue<VMap extends Record<string, any>> {
|
|
|
268
272
|
* Typically used for boolean "request" bits.
|
|
269
273
|
*/
|
|
270
274
|
tap: <K extends keyof VMap & string>(tagName: K) => Promise<void>;
|
|
275
|
+
/** Sets boolean tag to true. */
|
|
276
|
+
press: <K extends keyof VMap & string>(tagName: K) => Promise<void>;
|
|
277
|
+
/** Sets boolean tag to false. */
|
|
278
|
+
release: <K extends keyof VMap & string>(tagName: K) => Promise<void>;
|
|
271
279
|
/** Current scale configurations by name. */
|
|
272
280
|
scales: Record<string, ScaleConfig>;
|
|
273
281
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AutoCoreTagTypes.d.ts","sourceRoot":"","sources":["../../src/core/AutoCoreTagTypes.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AutoCoreTagTypes.d.ts","sourceRoot":"","sources":["../../src/core/AutoCoreTagTypes.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,8 @@ export declare function makeAutoCoreTagHooks<Spec extends readonly TagConfig[]>(
|
|
|
5
5
|
readonly rawValue: unknown;
|
|
6
6
|
readonly write: (displayValue: TagValueMap<Spec>[K]) => Promise<void>;
|
|
7
7
|
readonly tap: () => Promise<void>;
|
|
8
|
+
readonly press: () => Promise<void>;
|
|
9
|
+
readonly release: () => Promise<void>;
|
|
8
10
|
readonly isLoading: boolean;
|
|
9
11
|
};
|
|
10
12
|
readonly useAutoCoreTags: <K extends Spec[number]["tagName"]>(tagNames: readonly K[]) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutoCoreTag.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoCoreTag.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEzF,wBAAgB,oBAAoB,CAAC,IAAI,SAAS,SAAS,SAAS,EAAE,EAClE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAC3D,IAAI,EAAE,IAAI;8BAYc,CAAC,2CAA4B,CAAC
|
|
1
|
+
{"version":3,"file":"useAutoCoreTag.d.ts","sourceRoot":"","sources":["../../src/hooks/useAutoCoreTag.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEzF,wBAAgB,oBAAoB,CAAC,IAAI,SAAS,SAAS,SAAS,EAAE,EAClE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAC3D,IAAI,EAAE,IAAI;8BAYc,CAAC,2CAA4B,CAAC;;;;;;;;;+BA6B7B,CAAC,4CAA6B,SAAS,CAAC,EAAE;4BAO/C,CAAC;;kCAYyB,CAAC;;;iCAUpB,CAAC,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,mBAAM,CAAC,KAAK,CAAC,SAAQ,GAAG,EAAE;;;;;;;uCAW3C,MAAM;;EAKtD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{useCallback,useContext,useMemo}from"react";export function makeAutoCoreTagHooks(e,a){return{useAutoCoreTag:
|
|
1
|
+
import{useCallback,useContext,useMemo}from"react";export function makeAutoCoreTagHooks(e,a){return{useAutoCoreTag:s=>{const{values:t,rawValues:u,isLoading:o,write:r,tap:l,press:n,release:c}=useContext(e),i=(e=>a.find(a=>a.tagName===e))(s);if(!i)throw new Error(`useAutoCoreTag: unknown tagName '${String(s)}'`);const C=useCallback(e=>r(s,e),[r,s]),g=useCallback(()=>l?.(s),[l,s]),m=useCallback(()=>n?.(s),[n,s]),w=useCallback(()=>c?.(s),[c,s]);return{value:t[s],rawValue:u[s],write:C,tap:g,press:m,release:w,isLoading:o}},useAutoCoreTags:a=>{const{values:s,rawValues:t,isLoading:u,write:o}=useContext(e);return{values:useMemo(()=>Object.fromEntries(a.map(e=>[e,s[e]])),[s,a]),rawValues:useMemo(()=>Object.fromEntries(a.map(e=>[e,t[e]])),[t,a]),write:useCallback(async(e,a)=>{await o(e,a)},[o]),isLoading:u}},useAutoCoreSelect:(a,s=[])=>{const{values:t,isLoading:u}=useContext(e);return{selected:useMemo(()=>a(t),[t,...s]),isLoading:u}},useScales:()=>{const{scales:a,updateScale:s}=useContext(e),t=useCallback(e=>a[e],[a]);return{scales:a,updateScale:s,getScale:t}}}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adcops/autocore-react",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.36",
|
|
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",
|
|
@@ -69,6 +69,19 @@ export interface IndicatorButtonProps extends Omit<ButtonProps, "value"> {
|
|
|
69
69
|
* cycle on onColor.
|
|
70
70
|
*/
|
|
71
71
|
flash?: boolean;
|
|
72
|
+
|
|
73
|
+
/** Callback fired when the button is pressed (mouse down or touch start). */
|
|
74
|
+
onPress?: (e: React.MouseEvent | React.TouchEvent) => void;
|
|
75
|
+
|
|
76
|
+
/** Callback fired when the button is released (mouse up, touch end, or mouse leave). */
|
|
77
|
+
onRelease?: (e: React.MouseEvent | React.TouchEvent) => void;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Momentary tag writer. When the button is pressed, this is called with `true`.
|
|
81
|
+
* When the button is released or the mouse leaves, it is called with `false`.
|
|
82
|
+
* Pass the `write` function from `useAutoCoreTag` directly to this prop.
|
|
83
|
+
*/
|
|
84
|
+
momentary?: (value: boolean) => void | Promise<void>;
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
const toPrimeIcon = (cls?: string) =>
|
|
@@ -94,6 +107,9 @@ export const IndicatorButton: React.FC<IndicatorButtonProps> = ({
|
|
|
94
107
|
disabled,
|
|
95
108
|
hidden,
|
|
96
109
|
flash,
|
|
110
|
+
onPress,
|
|
111
|
+
onRelease,
|
|
112
|
+
momentary,
|
|
97
113
|
...restProps
|
|
98
114
|
}) => {
|
|
99
115
|
const { subscribe, unsubscribe, dispatch } = useContext(
|
|
@@ -179,25 +195,31 @@ export const IndicatorButton: React.FC<IndicatorButtonProps> = ({
|
|
|
179
195
|
[command, dispatch, invert, commandArgs, commandTopic]
|
|
180
196
|
);
|
|
181
197
|
|
|
182
|
-
const handleOnPressed = useCallback(() => {
|
|
183
|
-
if (isPressed) return;
|
|
198
|
+
const handleOnPressed = useCallback((e: React.MouseEvent | React.TouchEvent) => {
|
|
199
|
+
if (isPressed || disabled) return;
|
|
184
200
|
setIsPressed(true);
|
|
185
201
|
|
|
202
|
+
if (momentary) momentary(true);
|
|
203
|
+
if (onPress) onPress(e);
|
|
204
|
+
|
|
186
205
|
if (actionMode === ActionMode.Tap || actionMode === ActionMode.Pressed) {
|
|
187
206
|
dispatchCommand(true);
|
|
188
207
|
}
|
|
189
|
-
}, [isPressed, actionMode, dispatchCommand]);
|
|
208
|
+
}, [isPressed, disabled, actionMode, dispatchCommand, onPress, momentary]);
|
|
190
209
|
|
|
191
|
-
const handleOnReleased = useCallback(() => {
|
|
192
|
-
if (!isPressed) return;
|
|
210
|
+
const handleOnReleased = useCallback((e: React.MouseEvent | React.TouchEvent) => {
|
|
211
|
+
if (!isPressed || disabled) return;
|
|
193
212
|
setIsPressed(false);
|
|
194
213
|
|
|
214
|
+
if (momentary) momentary(false);
|
|
215
|
+
if (onRelease) onRelease(e);
|
|
216
|
+
|
|
195
217
|
if (actionMode === ActionMode.Tap) {
|
|
196
218
|
dispatchCommand(false);
|
|
197
219
|
} else if (actionMode === ActionMode.Released) {
|
|
198
220
|
dispatchCommand(true);
|
|
199
221
|
}
|
|
200
|
-
}, [isPressed, actionMode, dispatchCommand]);
|
|
222
|
+
}, [isPressed, disabled, actionMode, dispatchCommand, onRelease, momentary]);
|
|
201
223
|
|
|
202
224
|
const mergedStyle: React.CSSProperties = {
|
|
203
225
|
color: "white",
|
|
@@ -224,6 +246,7 @@ export const IndicatorButton: React.FC<IndicatorButtonProps> = ({
|
|
|
224
246
|
onTouchStart={handleOnPressed}
|
|
225
247
|
onMouseUp={handleOnReleased}
|
|
226
248
|
onTouchEnd={handleOnReleased}
|
|
249
|
+
onMouseLeave={handleOnReleased} // Safety net for drag-offs
|
|
227
250
|
/>
|
|
228
251
|
);
|
|
229
252
|
};
|
|
@@ -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(' / ') ?? '';
|
|
@@ -104,6 +104,8 @@ export const AutoCoreTagContext = createContext<BaseContextValue<VMapRuntime>>({
|
|
|
104
104
|
isLoading: true,
|
|
105
105
|
write: async () => { },
|
|
106
106
|
tap: async () => { },
|
|
107
|
+
press: async () => { },
|
|
108
|
+
release: async () => { },
|
|
107
109
|
scales: {},
|
|
108
110
|
updateScale: async () => { },
|
|
109
111
|
});
|
|
@@ -538,6 +540,8 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
538
540
|
|
|
539
541
|
/**
|
|
540
542
|
* Momentary pulse for boolean tags (True -> Wait 300ms -> False).
|
|
543
|
+
* Now uses try...finally to ensure the tag is always reset to false
|
|
544
|
+
* even if the connection is lost or an error occurs during the pulse.
|
|
541
545
|
*/
|
|
542
546
|
const tap = useCallback(
|
|
543
547
|
async (tagName: string) => {
|
|
@@ -551,9 +555,40 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
551
555
|
return;
|
|
552
556
|
}
|
|
553
557
|
|
|
554
|
-
|
|
558
|
+
// Fire and forget the writes. WebSockets guarantee ordered delivery.
|
|
559
|
+
// We don't await them here so that the pulse is responsive and
|
|
560
|
+
// doesn't hang if the server is slow to acknowledge the first write.
|
|
561
|
+
hubWrite(cfg.fqdn, true).catch(e => console.error(`tap(): write true failed`, e));
|
|
562
|
+
|
|
555
563
|
await sleep(300);
|
|
556
|
-
|
|
564
|
+
|
|
565
|
+
hubWrite(cfg.fqdn, false).catch(e => console.error(`tap(): write false failed`, e));
|
|
566
|
+
},
|
|
567
|
+
[tags, hubWrite]
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Sets a boolean tag to TRUE.
|
|
572
|
+
*/
|
|
573
|
+
const press = useCallback(
|
|
574
|
+
async (tagName: string) => {
|
|
575
|
+
const cfg = tags.find((t) => t.tagName === tagName);
|
|
576
|
+
if (cfg && cfg.valueType === "boolean") {
|
|
577
|
+
await hubWrite(cfg.fqdn, true);
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
[tags, hubWrite]
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Sets a boolean tag to FALSE.
|
|
585
|
+
*/
|
|
586
|
+
const release = useCallback(
|
|
587
|
+
async (tagName: string) => {
|
|
588
|
+
const cfg = tags.find((t) => t.tagName === tagName);
|
|
589
|
+
if (cfg && cfg.valueType === "boolean") {
|
|
590
|
+
await hubWrite(cfg.fqdn, false);
|
|
591
|
+
}
|
|
557
592
|
},
|
|
558
593
|
[tags, hubWrite]
|
|
559
594
|
);
|
|
@@ -612,11 +647,12 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
612
647
|
isLoading,
|
|
613
648
|
write,
|
|
614
649
|
tap,
|
|
650
|
+
press,
|
|
651
|
+
release,
|
|
615
652
|
scales: scaleValues,
|
|
616
653
|
updateScale,
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
);
|
|
654
|
+
}), [values, rawValues, isLoading, write, tap, press, release, scaleValues, updateScale]);
|
|
655
|
+
|
|
620
656
|
|
|
621
657
|
return (
|
|
622
658
|
<AutoCoreTagContext.Provider value={ctxValue}>
|
|
@@ -58,6 +58,10 @@ export type TagBinding<T> = Readonly<{
|
|
|
58
58
|
write: (displayValue: T) => Promise<void>;
|
|
59
59
|
/** Momentary action; typically boolean true→false pulse. */
|
|
60
60
|
tap: () => Promise<void>;
|
|
61
|
+
/** Sets boolean tag to true. */
|
|
62
|
+
press: () => Promise<void>;
|
|
63
|
+
/** Sets boolean tag to false. */
|
|
64
|
+
release: () => Promise<void>;
|
|
61
65
|
/** True while initial read/bootstrapping is in progress. */
|
|
62
66
|
isLoading: boolean;
|
|
63
67
|
}>;
|
|
@@ -320,6 +324,12 @@ export interface BaseContextValue<VMap extends Record<string, any>> {
|
|
|
320
324
|
*/
|
|
321
325
|
tap: <K extends keyof VMap & string>(tagName: K) => Promise<void>;
|
|
322
326
|
|
|
327
|
+
/** Sets boolean tag to true. */
|
|
328
|
+
press: <K extends keyof VMap & string>(tagName: K) => Promise<void>;
|
|
329
|
+
|
|
330
|
+
/** Sets boolean tag to false. */
|
|
331
|
+
release: <K extends keyof VMap & string>(tagName: K) => Promise<void>;
|
|
332
|
+
|
|
323
333
|
/** Current scale configurations by name. */
|
|
324
334
|
scales: Record<string, ScaleConfig>;
|
|
325
335
|
|
|
@@ -31,7 +31,7 @@ export function makeAutoCoreTagHooks<Spec extends readonly TagConfig[]>(
|
|
|
31
31
|
* Returns current value, raw value, direct server writer, and tap helper.
|
|
32
32
|
*/
|
|
33
33
|
const useAutoCoreTag = <K extends TagNames>(tagName: K) => {
|
|
34
|
-
const { values, rawValues, isLoading, write: ctxWrite, tap: ctxTap } = useContext(Context);
|
|
34
|
+
const { values, rawValues, isLoading, write: ctxWrite, tap: ctxTap, press: ctxPress, release: ctxRelease } = useContext(Context);
|
|
35
35
|
|
|
36
36
|
const cfg = findCfg(tagName as string);
|
|
37
37
|
if (!cfg) throw new Error(`useAutoCoreTag: unknown tagName '${String(tagName)}'`);
|
|
@@ -42,12 +42,16 @@ export function makeAutoCoreTagHooks<Spec extends readonly TagConfig[]>(
|
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
const tap = useCallback(() => ctxTap?.(tagName), [ctxTap, tagName]);
|
|
45
|
+
const press = useCallback(() => ctxPress?.(tagName), [ctxPress, tagName]);
|
|
46
|
+
const release = useCallback(() => ctxRelease?.(tagName), [ctxRelease, tagName]);
|
|
45
47
|
|
|
46
48
|
return {
|
|
47
49
|
value: values[tagName] as VMap[K],
|
|
48
50
|
rawValue: rawValues[tagName as string],
|
|
49
51
|
write,
|
|
50
52
|
tap,
|
|
53
|
+
press,
|
|
54
|
+
release,
|
|
51
55
|
isLoading,
|
|
52
56
|
} as const;
|
|
53
57
|
};
|