@adcops/autocore-react 3.3.79 → 3.3.83

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.
Files changed (29) hide show
  1. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  2. package/dist/components/ams/AmsProvider.js +1 -1
  3. package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
  4. package/dist/components/ams/AssetDetailView.js +1 -1
  5. package/dist/components/ams/CalibrationEntryDialog.d.ts +15 -0
  6. package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -1
  7. package/dist/components/ams/CalibrationEntryDialog.js +1 -1
  8. package/dist/components/tis/TestDataView.d.ts +3 -0
  9. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  10. package/dist/components/tis/TestDataView.js +1 -1
  11. package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
  12. package/dist/components/tis/TestRawDataView.js +1 -1
  13. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  14. package/dist/components/tis/TestSetupForm.js +1 -1
  15. package/dist/components/tis/TisProvider.d.ts +24 -0
  16. package/dist/components/tis/TisProvider.d.ts.map +1 -1
  17. package/dist/components/tis/TisProvider.js +1 -1
  18. package/dist/components/tis/useRawCycleData.d.ts +39 -0
  19. package/dist/components/tis/useRawCycleData.d.ts.map +1 -0
  20. package/dist/components/tis/useRawCycleData.js +1 -0
  21. package/package.json +1 -1
  22. package/src/components/ams/AmsProvider.tsx +4 -3
  23. package/src/components/ams/AssetDetailView.tsx +24 -2
  24. package/src/components/ams/CalibrationEntryDialog.tsx +75 -13
  25. package/src/components/tis/TestDataView.tsx +176 -68
  26. package/src/components/tis/TestRawDataView.tsx +15 -97
  27. package/src/components/tis/TestSetupForm.tsx +25 -9
  28. package/src/components/tis/TisProvider.tsx +33 -2
  29. package/src/components/tis/useRawCycleData.ts +157 -0
@@ -10,7 +10,7 @@
10
10
  * built-in dialog inside <TestDataView>.
11
11
  */
12
12
 
13
- import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
13
+ import React, { useMemo, useRef, useState } from 'react';
14
14
  import { Button } from 'primereact/button';
15
15
  import { Dropdown } from 'primereact/dropdown';
16
16
 
@@ -21,10 +21,9 @@ import { Chart as ChartJS,
21
21
  import zoomPlugin from 'chartjs-plugin-zoom';
22
22
  import { Line } from 'react-chartjs-2';
23
23
 
24
- import { EventEmitterContext } from '../../core/EventEmitterContext';
25
- import { MessageType } from '../../hub/CommandMessage';
26
24
  import type { ChartView, TestMethod } from './TestDataView';
27
25
  import { useTis } from './TisProvider';
26
+ import { useRawCycleData } from './useRawCycleData';
28
27
 
29
28
  ChartJS.register(
30
29
  CategoryScale, LinearScale, PointElement, LineElement,
@@ -53,19 +52,8 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
53
52
  const runId = props.runId ?? tis.selection.runId;
54
53
  const schema = props.schema ?? (methodId ? (tis.schemas[methodId] as TestMethod) : undefined);
55
54
  const { blobName, chartHeight = '60vh' } = props;
56
- const { invoke } = useContext(EventEmitterContext);
57
-
58
- const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
59
- const [envelope, setEnvelope] = useState<any | null>(null);
60
- const [loading, setLoading] = useState(true);
61
- const [error, setError] = useState<string | null>(null);
62
55
  const chartRef = useRef<any>(null);
63
56
 
64
- // Per-test cycle picker. Default to latest cycle on disk; the
65
- // operator can flip backward through earlier cycles.
66
- const [cycles, setCycles] = useState<number[]>([]);
67
- const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
68
-
69
57
  // raw_trace-capable views only — cycle scatter lives in <TestDataView>.
70
58
  const traceViews = useMemo(() => {
71
59
  const out: { name: string; view: ChartView }[] = [];
@@ -81,75 +69,19 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
81
69
 
82
70
  const effectiveBlobName = blobName ?? schema?.raw_data?.blob_name ?? 'trace';
83
71
 
84
- // Reset cycle state when the run identity changes; the new run has
85
- // its own cycle list and "latest" target.
86
- useEffect(() => {
87
- setCycles([]);
88
- setSelectedCycle(null);
89
- }, [projectId, methodId, runId, effectiveBlobName]);
90
-
91
- // Discover available cycles for this run/blob. Runs ahead of the
92
- // data fetch so the cycle picker can render immediately.
93
- useEffect(() => {
94
- if (!projectId || !methodId || !runId) return;
95
- let cancelled = false;
96
- (async () => {
97
- try {
98
- const resp: any = await invoke(
99
- 'tis.list_raw' as any, MessageType.Request as any,
100
- { project_id: projectId, method_id: methodId, run_id: runId } as any);
101
- if (cancelled || !resp?.success) return;
102
- const list: any[] = resp.data?.cycles ?? [];
103
- const indices = list
104
- .filter(c => c?.name === effectiveBlobName && typeof c?.cycle_index === 'number')
105
- .map(c => c.cycle_index as number)
106
- .sort((a, b) => a - b);
107
- setCycles(indices);
108
- if (indices.length > 0) {
109
- setSelectedCycle(prev => prev ?? indices[indices.length - 1]);
110
- }
111
- } catch {
112
- // Listing failure is non-fatal — the data fetch below
113
- // still tries "latest" and the picker just stays empty.
114
- }
115
- })();
116
- return () => { cancelled = true; };
117
- }, [projectId, methodId, runId, effectiveBlobName, invoke]);
118
-
119
- // Lazy fetch — runs on mount / when identifiers / selectedCycle change.
120
- useEffect(() => {
121
- if (!projectId || !methodId || !runId) {
122
- setRaw(null); setEnvelope(null); setLoading(false); setError(null);
123
- return;
124
- }
125
- let cancelled = false;
126
- setLoading(true);
127
- setError(null);
128
- (async () => {
129
- try {
130
- const args: Record<string, any> = {
131
- project_id: projectId, method_id: methodId,
132
- run_id: runId, name: effectiveBlobName,
133
- };
134
- if (selectedCycle != null) args.cycle_index = selectedCycle;
135
- const resp: any = await invoke(
136
- 'tis.read_raw' as any, MessageType.Request as any, args as any);
137
- if (cancelled) return;
138
- if (resp?.success) {
139
- const payload = resp.data ?? {};
140
- setEnvelope(payload);
141
- setRaw(unwrapEnvelope(payload));
142
- } else {
143
- setError(resp?.error_message ?? 'Failed to read raw data');
144
- }
145
- } catch (e: any) {
146
- if (!cancelled) setError(String(e?.message ?? e));
147
- } finally {
148
- if (!cancelled) setLoading(false);
149
- }
150
- })();
151
- return () => { cancelled = true; };
152
- }, [projectId, methodId, runId, effectiveBlobName, selectedCycle, invoke]);
72
+ // Cycle discovery + per-cycle blob fetch live in the shared hook so
73
+ // <TestDataView>'s unified panel can reuse the exact same fetch path
74
+ // when a raw_trace view is selected there.
75
+ const { cycles, selectedCycle, setSelectedCycle, raw, envelope, loading, error } =
76
+ useRawCycleData({
77
+ projectId, methodId, runId,
78
+ blobName: effectiveBlobName,
79
+ // This component only renders raw_trace charts, so always
80
+ // fetch as long as the run identity is in scope. The early
81
+ // returns below cover the "no test selected" / "no schema"
82
+ // cases that would otherwise be wasted requests.
83
+ enabled: !!projectId && !!methodId && !!runId,
84
+ });
153
85
 
154
86
  const chartData = useMemo(() => {
155
87
  if (!raw || !selectedView) return null;
@@ -279,20 +211,6 @@ const EmptyState: React.FC<{ message: string }> = ({ message }) => (
279
211
  <div style={{ padding: '1rem', color: 'var(--text-secondary-color)' }}>{message}</div>
280
212
  );
281
213
 
282
- // tis.read_raw returns one of:
283
- // - per-cycle envelope: { cycle_index, cycle_fields, context, data }
284
- // - legacy flat blob: { col: number[] }
285
- // Chart/CSV code only wants the columnar payload — strip the envelope
286
- // when present, otherwise pass the blob through.
287
- const unwrapEnvelope = (blob: any): Record<string, number[]> => {
288
- if (!blob || typeof blob !== 'object') return {};
289
- if ('data' in blob && blob.data && typeof blob.data === 'object'
290
- && Object.values(blob.data).some(v => Array.isArray(v))) {
291
- return blob.data;
292
- }
293
- return blob;
294
- };
295
-
296
214
  // Strip of cycle_index + cycle_fields + context, rendered above the
297
215
  // chart so the operator can see *which* cycle they're looking at and
298
216
  // what schema-declared metric values were active for it. Renders
@@ -368,19 +368,35 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
368
368
  };
369
369
 
370
370
  /**
371
- * If the test method has an `asset_ref` whose `select=by_id_field`
372
- * pulls from this config field, return the asset_type the field
373
- * should pick from. The form renders that field as a dropdown of
374
- * AMS assets instead of a free-form text input.
371
+ * If any `asset_ref` declares `select=by_id_field` with
372
+ * `from: "config.<field_name>"` for this field, return the
373
+ * asset_type the field should pick from. The form renders that
374
+ * field as a dropdown of AMS assets instead of a free-form text
375
+ * input.
375
376
  *
376
- * The schema already encodes the relationship via `from:
377
- * "config.<field_name>"` no project.json change required.
377
+ * Refs are sourced from two places, matching what the server's
378
+ * resolver does at stage time:
379
+ * 1. Method-level (`schema.asset_refs`) — method-specific
380
+ * accessories.
381
+ * 2. Project-level (`tis.projectAssetRefs`) — system hardware
382
+ * that applies to every test method (e.g. the test surface
383
+ * picked per-test by id).
384
+ * Method-level wins on `field`-name collision, so a method can
385
+ * override the project default.
378
386
  */
379
387
  const assetTypeForField = (field: TestFieldDef): string | null => {
380
- const refs = (schema?.asset_refs ?? []) as TisAssetRef[];
388
+ const methodRefs = (schema?.asset_refs ?? []) as TisAssetRef[];
389
+ const projectRefs = (tis.projectAssetRefs ?? []) as TisAssetRef[];
381
390
  const expectedFrom = `config.${field.name}`;
382
- const hit = refs.find(r => r.select === 'by_id_field' && r.from === expectedFrom);
383
- return hit ? hit.asset_type : null;
391
+ // Method wins on collision iterate method refs first.
392
+ const methodHit = methodRefs.find(
393
+ r => r.select === 'by_id_field' && r.from === expectedFrom,
394
+ );
395
+ if (methodHit) return methodHit.asset_type;
396
+ const projectHit = projectRefs.find(
397
+ r => r.select === 'by_id_field' && r.from === expectedFrom,
398
+ );
399
+ return projectHit ? projectHit.asset_type : null;
384
400
  };
385
401
 
386
402
  const renderConfigField = (field: TestFieldDef) => {
@@ -91,8 +91,33 @@ export interface TisRunCacheEntry {
91
91
  rawData: { [blobName: string]: any };
92
92
  }
93
93
 
94
+ /**
95
+ * One project-level asset_ref entry, in the wire shape returned by
96
+ * `tis.list_schemas`. Mirrors `Project.asset_refs` on the server.
97
+ * Carries only the fields the form's by_id_field lookup needs;
98
+ * other fields (label, description, calibration_required) are
99
+ * ignored but pass through untouched.
100
+ */
101
+ export interface TisProjectAssetRef {
102
+ field: string;
103
+ asset_type: string;
104
+ select: 'by_location' | 'by_id_field';
105
+ /** Set when `select=by_id_field` — the dotted config path
106
+ * (`config.<field_name>`) whose value picks the asset. */
107
+ from?: string;
108
+ /** Set when `select=by_location` — the role/location key. */
109
+ location?: string;
110
+ }
111
+
94
112
  export interface TisContextValue {
95
113
  schemas: SchemaRegistry;
114
+ /** Project-level asset_refs from `tis.list_schemas`. Empty array
115
+ * when the project declares none. The HMI form merges these with
116
+ * the active method's own `asset_refs` before deciding whether to
117
+ * render a config field as a dropdown (by_id_field) or a free-form
118
+ * input. Method-level refs win on `field`-name collision, mirroring
119
+ * the server resolver. */
120
+ projectAssetRefs: TisProjectAssetRef[];
96
121
  defaultMethodId: string;
97
122
  schemasLoaded: boolean;
98
123
 
@@ -182,6 +207,7 @@ const EMPTY_SELECTION: TisSelection = {
182
207
 
183
208
  const TisContext = createContext<TisContextValue>({
184
209
  schemas: {},
210
+ projectAssetRefs: [],
185
211
  defaultMethodId: '',
186
212
  schemasLoaded: false,
187
213
  state: EMPTY_STATE,
@@ -252,6 +278,7 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
252
278
  const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
253
279
 
254
280
  const [schemas, setSchemas] = useState<SchemaRegistry>({});
281
+ const [projectAssetRefs, setProjectAssetRefs] = useState<TisProjectAssetRef[]>([]);
255
282
  const [defaultMethodId, setDefaultMethodId] = useState<string>(initialDefault ?? '');
256
283
  const [schemasLoaded, setSchemasLoaded] = useState(false);
257
284
 
@@ -284,7 +311,11 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
284
311
  if (resp?.success && resp.data) {
285
312
  const methods = (resp.data.test_methods ?? {}) as SchemaRegistry;
286
313
  const dflt = (resp.data.default_method_id ?? '') as string;
314
+ const refs = Array.isArray(resp.data.asset_refs)
315
+ ? (resp.data.asset_refs as TisProjectAssetRef[])
316
+ : [];
287
317
  setSchemas(methods);
318
+ setProjectAssetRefs(refs);
288
319
  if (!initialDefault && dflt) setDefaultMethodId(dflt);
289
320
  setSchemasLoaded(true);
290
321
  } else {
@@ -620,14 +651,14 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
620
651
  }, []);
621
652
 
622
653
  const value: TisContextValue = useMemo(() => ({
623
- schemas, defaultMethodId, schemasLoaded,
654
+ schemas, projectAssetRefs, defaultMethodId, schemasLoaded,
624
655
  state, selection, setSelection,
625
656
  existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
626
657
  projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
627
658
  stagedConfig, setStagedConfig, clearStagedConfig,
628
659
  fetchRuns, fetchRun, runCache,
629
660
  }), [
630
- schemas, defaultMethodId, schemasLoaded,
661
+ schemas, projectAssetRefs, defaultMethodId, schemasLoaded,
631
662
  state, selection, setSelection,
632
663
  existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
633
664
  projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
@@ -0,0 +1,157 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * useRawCycleData — shared hook that fetches `tis.list_raw` + `tis.read_raw`
5
+ * for one (project, method, run, blob_name) slice and tracks the
6
+ * operator's per-cycle selection. Used by:
7
+ *
8
+ * - <TestRawDataView> — the focused trace-only viewer.
9
+ * - <TestDataView> — the unified chart panel, which lazily fetches
10
+ * raw data only when a raw_trace view is selected
11
+ * (set `enabled` to false to short-circuit while
12
+ * a cycle_scatter view is active).
13
+ *
14
+ * Keeping the fetch logic in one place means the two components can't
15
+ * drift in how they discover cycles, default to "latest cycle", or
16
+ * unwrap the per-cycle envelope.
17
+ */
18
+
19
+ import { useContext, useEffect, useState } from 'react';
20
+ import { EventEmitterContext } from '../../core/EventEmitterContext';
21
+ import { MessageType } from '../../hub/CommandMessage';
22
+
23
+ export interface UseRawCycleDataOptions {
24
+ projectId?: string;
25
+ methodId?: string;
26
+ runId?: string;
27
+ /** Blob name (e.g. "trace"); usually schema.raw_data.blob_name. */
28
+ blobName: string;
29
+ /** When false, the hook does no fetching and returns empty state.
30
+ * Wire to "is the current chart view a raw_trace?" so scatter
31
+ * selections don't trigger a blob round-trip. */
32
+ enabled: boolean;
33
+ }
34
+
35
+ export interface UseRawCycleDataResult {
36
+ /** Per-cycle indices discovered on disk for this run/blob, sorted
37
+ * ascending. Empty when no raw_data has been written yet. */
38
+ cycles: number[];
39
+ selectedCycle: number | null;
40
+ setSelectedCycle: (n: number | null) => void;
41
+ /** Columnar payload unwrapped from the per-cycle envelope, or the
42
+ * blob itself for legacy flat shapes. `null` until the fetch lands. */
43
+ raw: Record<string, number[]> | null;
44
+ /** Full envelope as returned by `tis.read_raw`. Carries
45
+ * `cycle_index`, `cycle_fields`, and `context` alongside `data`. */
46
+ envelope: any | null;
47
+ loading: boolean;
48
+ error: string | null;
49
+ }
50
+
51
+ export function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataResult {
52
+ const { projectId, methodId, runId, blobName, enabled } = opts;
53
+ const { invoke } = useContext(EventEmitterContext);
54
+
55
+ const [cycles, setCycles] = useState<number[]>([]);
56
+ const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
57
+ const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
58
+ const [envelope, setEnvelope] = useState<any | null>(null);
59
+ const [loading, setLoading] = useState(false);
60
+ const [error, setError] = useState<string | null>(null);
61
+
62
+ // Reset cycle state when the run identity (or blob) changes — the
63
+ // new slice has its own cycle list and its own "latest" target.
64
+ useEffect(() => {
65
+ setCycles([]);
66
+ setSelectedCycle(null);
67
+ setRaw(null);
68
+ setEnvelope(null);
69
+ setError(null);
70
+ }, [projectId, methodId, runId, blobName]);
71
+
72
+ // Cycle-list discovery. Runs ahead of the data fetch so the cycle
73
+ // picker can render immediately. Filtered to the requested blob name.
74
+ useEffect(() => {
75
+ if (!enabled || !projectId || !methodId || !runId) return;
76
+ let cancelled = false;
77
+ (async () => {
78
+ try {
79
+ const resp: any = await invoke(
80
+ 'tis.list_raw' as any, MessageType.Request as any,
81
+ { project_id: projectId, method_id: methodId, run_id: runId } as any,
82
+ );
83
+ if (cancelled || !resp?.success) return;
84
+ const list: any[] = resp.data?.cycles ?? [];
85
+ const indices = list
86
+ .filter(c => c?.name === blobName && typeof c?.cycle_index === 'number')
87
+ .map(c => c.cycle_index as number)
88
+ .sort((a, b) => a - b);
89
+ setCycles(indices);
90
+ if (indices.length > 0) {
91
+ setSelectedCycle(prev => prev ?? indices[indices.length - 1]);
92
+ }
93
+ } catch {
94
+ // Listing failure is non-fatal — the data fetch below
95
+ // still tries "latest" and the picker just stays empty.
96
+ }
97
+ })();
98
+ return () => { cancelled = true; };
99
+ }, [enabled, projectId, methodId, runId, blobName, invoke]);
100
+
101
+ // Lazy blob fetch — runs whenever identifiers / selectedCycle change.
102
+ useEffect(() => {
103
+ if (!enabled || !projectId || !methodId || !runId) {
104
+ setRaw(null); setEnvelope(null); setLoading(false); setError(null);
105
+ return;
106
+ }
107
+ let cancelled = false;
108
+ setLoading(true);
109
+ setError(null);
110
+ (async () => {
111
+ try {
112
+ const args: Record<string, any> = {
113
+ project_id: projectId, method_id: methodId,
114
+ run_id: runId, name: blobName,
115
+ };
116
+ if (selectedCycle != null) args.cycle_index = selectedCycle;
117
+ const resp: any = await invoke(
118
+ 'tis.read_raw' as any, MessageType.Request as any, args as any,
119
+ );
120
+ if (cancelled) return;
121
+ if (resp?.success) {
122
+ const payload = resp.data ?? {};
123
+ setEnvelope(payload);
124
+ setRaw(unwrapEnvelope(payload));
125
+ } else {
126
+ setError(resp?.error_message ?? 'Failed to read raw data');
127
+ }
128
+ } catch (e: any) {
129
+ if (!cancelled) setError(String(e?.message ?? e));
130
+ } finally {
131
+ if (!cancelled) setLoading(false);
132
+ }
133
+ })();
134
+ return () => { cancelled = true; };
135
+ }, [enabled, projectId, methodId, runId, blobName, selectedCycle, invoke]);
136
+
137
+ return { cycles, selectedCycle, setSelectedCycle, raw, envelope, loading, error };
138
+ }
139
+
140
+ /**
141
+ * `tis.read_raw` returns one of:
142
+ * - per-cycle envelope: `{ cycle_index, cycle_fields, context, data: { col: number[] } }`
143
+ * - legacy flat blob: `{ col: number[] }`
144
+ *
145
+ * Chart and CSV code only care about the columnar payload — peel off
146
+ * the envelope when present, otherwise pass the blob through. Returned
147
+ * shape is always `Record<string, number[]>` so consumers can index
148
+ * uniformly.
149
+ */
150
+ export function unwrapEnvelope(blob: any): Record<string, number[]> {
151
+ if (!blob || typeof blob !== 'object') return {};
152
+ if ('data' in blob && blob.data && typeof blob.data === 'object'
153
+ && Object.values(blob.data).some(v => Array.isArray(v))) {
154
+ return blob.data;
155
+ }
156
+ return blob;
157
+ }