@adcops/autocore-react 3.3.82 → 3.3.84

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 (30) hide show
  1. package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
  2. package/dist/components/ams/AssetDetailView.js +1 -1
  3. package/dist/components/ams/AssetEditDialog.d.ts +13 -0
  4. package/dist/components/ams/AssetEditDialog.d.ts.map +1 -0
  5. package/dist/components/ams/AssetEditDialog.js +1 -0
  6. package/dist/components/ams/index.d.ts +1 -0
  7. package/dist/components/ams/index.d.ts.map +1 -1
  8. package/dist/components/ams/index.js +1 -1
  9. package/dist/components/network/NetworkPanel.d.ts.map +1 -1
  10. package/dist/components/network/NetworkPanel.js +1 -1
  11. package/dist/components/tis/TestDataView.d.ts +8 -0
  12. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  13. package/dist/components/tis/TestDataView.js +1 -1
  14. package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
  15. package/dist/components/tis/TestRawDataView.js +1 -1
  16. package/dist/components/tis/TestSetupForm.d.ts +15 -1
  17. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  18. package/dist/components/tis/TestSetupForm.js +1 -1
  19. package/dist/components/tis/useRawCycleData.d.ts +39 -0
  20. package/dist/components/tis/useRawCycleData.d.ts.map +1 -0
  21. package/dist/components/tis/useRawCycleData.js +1 -0
  22. package/package.json +1 -1
  23. package/src/components/ams/AssetDetailView.tsx +31 -0
  24. package/src/components/ams/AssetEditDialog.tsx +463 -0
  25. package/src/components/ams/index.ts +1 -0
  26. package/src/components/network/NetworkPanel.tsx +13 -1
  27. package/src/components/tis/TestDataView.tsx +256 -84
  28. package/src/components/tis/TestRawDataView.tsx +15 -97
  29. package/src/components/tis/TestSetupForm.tsx +60 -6
  30. package/src/components/tis/useRawCycleData.ts +258 -0
@@ -43,8 +43,22 @@ export interface TestFieldDef {
43
43
  * the control program sees it; for non-source fields it's stashed
44
44
  * straight into stagedConfig. Operator edits override per-stage,
45
45
  * but the schema default never mutates — re-selecting the method
46
- * re-applies the default. */
46
+ * re-applies the default. **Authored in display units** when
47
+ * `scale` is set — the form divides by `scale` before writing to
48
+ * GM / stagedConfig so the underlying storage stays raw. */
47
49
  default?: any;
50
+ /** Optional unit-conversion multiplier. Mirrors AutoCoreTagContext:
51
+ * ```
52
+ * display = raw * scale
53
+ * raw = display / scale
54
+ * ```
55
+ * Example: backend stores degrees, operator enters revolutions →
56
+ * `scale: 0.00277778`. None / 1.0 = no conversion.
57
+ *
58
+ * Storage stays raw — the form scales only on input and display.
59
+ * Cycle and results values are scaled by the corresponding paths
60
+ * in TestDataView; the server scales CSV exports too. */
61
+ scale?: number;
48
62
  }
49
63
 
50
64
  export interface TestMethod {
@@ -104,6 +118,30 @@ const labelOf = (f: TestFieldDef): string => {
104
118
  return f.units ? `${base} [${f.units}]` : base;
105
119
  };
106
120
 
121
+ /**
122
+ * Two helpers for the display ↔ raw boundary on numeric fields.
123
+ *
124
+ * Convention: `display = raw * scale`, `raw = display / scale`. Matches
125
+ * AutoCoreTagContext. Default scale = 1.0 (no conversion). Non-numeric
126
+ * input values (string, null, undefined, NaN) pass through unchanged
127
+ * — the form has fields of other types that share the change handler.
128
+ *
129
+ * Storage (config map, GM, on-disk JSON) always holds the raw value.
130
+ * Only the inputs the operator looks at and the cells in TestDataView
131
+ * use the display value. This way scales can change in project.json
132
+ * without invalidating historical records.
133
+ */
134
+ const rawToDisplay = (raw: any, scale: number | undefined): any => {
135
+ if (!scale || scale === 1) return raw;
136
+ if (typeof raw !== 'number' || !Number.isFinite(raw)) return raw;
137
+ return raw * scale;
138
+ };
139
+ const displayToRaw = (display: any, scale: number | undefined): any => {
140
+ if (!scale || scale === 1) return display;
141
+ if (typeof display !== 'number' || !Number.isFinite(display)) return display;
142
+ return display / scale;
143
+ };
144
+
107
145
  const hasDescription = (f: TestFieldDef): boolean =>
108
146
  typeof f.description === 'string' && f.description.length > 0;
109
147
 
@@ -265,14 +303,20 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
265
303
  if (field.name === 'sample_id') continue;
266
304
  if (field.default === undefined || field.default === null) continue;
267
305
  if (next === prev) next = { ...prev };
268
- next[field.name] = field.default;
306
+ // Schema defaults are authored in DISPLAY units (per
307
+ // the agreed convention) so the value the author reads
308
+ // in project.json matches the field's `units` label.
309
+ // Convert to raw before storing in stagedConfig / GM
310
+ // so the rest of the pipeline sees the canonical value.
311
+ const rawDefault = displayToRaw(field.default, field.scale);
312
+ next[field.name] = rawDefault;
269
313
  if (field.source) {
270
314
  // Mirror handleFieldChange: write to GM so the
271
315
  // control program sees the default. Errors here are
272
316
  // logged but non-fatal — the form still reflects
273
317
  // the default locally.
274
318
  void Promise.resolve()
275
- .then(() => write(field.source!, field.default))
319
+ .then(() => write(field.source!, rawDefault))
276
320
  .catch(e => console.error(
277
321
  `[TestSetupForm] Failed to seed default for ${field.name}:`, e));
278
322
  }
@@ -360,9 +404,13 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
360
404
  };
361
405
 
362
406
  const handleFieldChange = async (field: TestFieldDef, val: any) => {
363
- setConfig({ ...config, [field.name]: val });
407
+ // The operator typed a DISPLAY value; convert to RAW before
408
+ // storing or writing to GM. Non-numeric fields and identity
409
+ // scales pass through unchanged via the helper.
410
+ const rawVal = displayToRaw(val, field.scale);
411
+ setConfig({ ...config, [field.name]: rawVal });
364
412
  if (field.source) {
365
- try { await write(field.source, val); }
413
+ try { await write(field.source, rawVal); }
366
414
  catch (e) { console.error('Failed to write to source:', e); }
367
415
  }
368
416
  };
@@ -417,7 +465,13 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
417
465
  ) : isNum ? (
418
466
  <ValueInput
419
467
  label={undefined}
420
- value={config[field.name] != null ? Number(config[field.name]) : null}
468
+ // Storage is RAW; render the DISPLAY value so
469
+ // operator sees the units they configured. The
470
+ // change handler runs the inverse before
471
+ // committing back to storage.
472
+ value={config[field.name] != null
473
+ ? Number(rawToDisplay(Number(config[field.name]), field.scale))
474
+ : null}
421
475
  onValueChanged={(val) => handleFieldChange(field, val)}
422
476
  className={!valid ? 'p-invalid' : ''}
423
477
  />
@@ -0,0 +1,258 @@
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
+ * Live updates
15
+ * ------------
16
+ * The hook subscribes to the `tis.raw_data_added` broadcast and refreshes
17
+ * the cycle list whenever a new raw blob lands on disk for the matching
18
+ * (project, method, run, blob). When the operator is already viewing the
19
+ * latest cycle, the picker advances to the new latest and the chart
20
+ * re-paints — that's the customer-facing "chart updates each cycle"
21
+ * behaviour. When the operator has manually pinned an earlier cycle (e.g.
22
+ * examining cycle 3 while cycle 47 streams in), the picker stays put so
23
+ * the examination isn't interrupted.
24
+ *
25
+ * The hook intentionally suppresses `read_raw` until at least one cycle
26
+ * is known to be on disk for this slice. Otherwise the server returns
27
+ * "no raw_data file found" between `tis.start_test` and the first
28
+ * `add_raw_data` write, and the chart flashes an error overlay during
29
+ * what's really just "test just started, no data yet" — confusing for
30
+ * the operator. With the suppression the chart sits empty (or in a
31
+ * loading state) until the first raw_data_added broadcast arrives.
32
+ */
33
+
34
+ import { useCallback, useContext, useEffect, useRef, useState } from 'react';
35
+ import { EventEmitterContext } from '../../core/EventEmitterContext';
36
+ import { MessageType } from '../../hub/CommandMessage';
37
+
38
+ export interface UseRawCycleDataOptions {
39
+ projectId?: string;
40
+ methodId?: string;
41
+ runId?: string;
42
+ /** Blob name (e.g. "trace"); usually schema.raw_data.blob_name. */
43
+ blobName: string;
44
+ /** When false, the hook does no fetching and returns empty state.
45
+ * Wire to "is the current chart view a raw_trace?" so scatter
46
+ * selections don't trigger a blob round-trip. */
47
+ enabled: boolean;
48
+ }
49
+
50
+ export interface UseRawCycleDataResult {
51
+ /** Per-cycle indices discovered on disk for this run/blob, sorted
52
+ * ascending. Empty when no raw_data has been written yet. */
53
+ cycles: number[];
54
+ selectedCycle: number | null;
55
+ setSelectedCycle: (n: number | null) => void;
56
+ /** Columnar payload unwrapped from the per-cycle envelope, or the
57
+ * blob itself for legacy flat shapes. `null` until the fetch lands. */
58
+ raw: Record<string, number[]> | null;
59
+ /** Full envelope as returned by `tis.read_raw`. Carries
60
+ * `cycle_index`, `cycle_fields`, and `context` alongside `data`. */
61
+ envelope: any | null;
62
+ loading: boolean;
63
+ error: string | null;
64
+ }
65
+
66
+ export function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataResult {
67
+ const { projectId, methodId, runId, blobName, enabled } = opts;
68
+ const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
69
+
70
+ const [cycles, setCycles] = useState<number[]>([]);
71
+ const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
72
+ const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
73
+ const [envelope, setEnvelope] = useState<any | null>(null);
74
+ const [loading, setLoading] = useState(false);
75
+ const [error, setError] = useState<string | null>(null);
76
+
77
+ // Track whether the user has explicitly pinned a cycle. When false,
78
+ // the live-follow path is free to advance the picker to the newest
79
+ // cycle as it arrives. Flipping to true on a manual pick freezes
80
+ // the picker so examining cycle 3 while cycle 47 streams in doesn't
81
+ // get yanked back to 47.
82
+ //
83
+ // Lives in a ref so updating it doesn't re-trigger any effect — it's
84
+ // a behavioural latch, not state the renderer cares about.
85
+ const userPinnedRef = useRef<boolean>(false);
86
+
87
+ /** Discover the per-cycle indices currently on disk for this slice.
88
+ * Returns the sorted-ascending list; never throws (a listing error
89
+ * is logged-as-empty rather than surfaced — the rendering layer
90
+ * has its own empty state). */
91
+ const listCycles = useCallback(async (): Promise<number[]> => {
92
+ if (!projectId || !methodId || !runId) return [];
93
+ try {
94
+ const resp: any = await invoke(
95
+ 'tis.list_raw' as any, MessageType.Request as any,
96
+ { project_id: projectId, method_id: methodId, run_id: runId } as any,
97
+ );
98
+ if (!resp?.success) return [];
99
+ const list: any[] = resp.data?.cycles ?? [];
100
+ return list
101
+ .filter(c => c?.name === blobName && typeof c?.cycle_index === 'number')
102
+ .map(c => c.cycle_index as number)
103
+ .sort((a, b) => a - b);
104
+ } catch {
105
+ return [];
106
+ }
107
+ }, [projectId, methodId, runId, blobName, invoke]);
108
+
109
+ // Reset cycle state when the run identity (or blob) changes — the
110
+ // new slice has its own cycle list and its own "latest" target.
111
+ // Clearing userPinnedRef too so the next slice is free to follow
112
+ // the latest cycle automatically.
113
+ useEffect(() => {
114
+ setCycles([]);
115
+ setSelectedCycle(null);
116
+ setRaw(null);
117
+ setEnvelope(null);
118
+ setError(null);
119
+ userPinnedRef.current = false;
120
+ }, [projectId, methodId, runId, blobName]);
121
+
122
+ // Cycle-list discovery. Runs ahead of the data fetch so the cycle
123
+ // picker can render immediately. Filtered to the requested blob name.
124
+ useEffect(() => {
125
+ if (!enabled || !projectId || !methodId || !runId) return;
126
+ let cancelled = false;
127
+ (async () => {
128
+ const indices = await listCycles();
129
+ if (cancelled) return;
130
+ setCycles(indices);
131
+ if (indices.length > 0) {
132
+ setSelectedCycle(prev => prev ?? indices[indices.length - 1]);
133
+ }
134
+ })();
135
+ return () => { cancelled = true; };
136
+ }, [enabled, projectId, methodId, runId, blobName, listCycles]);
137
+
138
+ // Live updates: each new raw_data file landing on disk triggers a
139
+ // re-listing. The handler closes over `selectedCycle` via the
140
+ // functional setter and the live `cycles` snapshot, so we use refs
141
+ // to avoid re-subscribing every time those state values change.
142
+ const cyclesRef = useRef<number[]>(cycles);
143
+ cyclesRef.current = cycles;
144
+ const selectedRef = useRef<number | null>(selectedCycle);
145
+ selectedRef.current = selectedCycle;
146
+
147
+ useEffect(() => {
148
+ if (!enabled || !projectId || !methodId || !runId) return;
149
+ const onRawAdded = async (payload: any) => {
150
+ // Filter to the slice this hook instance is bound to.
151
+ // The server can also fire raw_data_added for the same
152
+ // run+method+project but a different blob name (rare, but
153
+ // legal); the blob-name check keeps us from refreshing
154
+ // for an unrelated dataset.
155
+ if (payload?.project_id !== projectId) return;
156
+ if (payload?.method_id !== methodId) return;
157
+ if (payload?.run_id !== runId) return;
158
+ if (payload?.name !== blobName) return;
159
+
160
+ const fresh = await listCycles();
161
+ if (fresh.length === 0) return;
162
+ setCycles(fresh);
163
+ // Auto-follow "latest" iff the operator hasn't manually
164
+ // moved away from it. The "was viewing the previous
165
+ // latest" check uses the snapshot taken via the ref so the
166
+ // closure stays stable.
167
+ const prevLatest = cyclesRef.current.length > 0
168
+ ? cyclesRef.current[cyclesRef.current.length - 1]
169
+ : null;
170
+ const newLatest = fresh[fresh.length - 1];
171
+ const onLatest = !userPinnedRef.current
172
+ && (selectedRef.current === null || selectedRef.current === prevLatest);
173
+ if (onLatest && newLatest !== selectedRef.current) {
174
+ setSelectedCycle(newLatest);
175
+ }
176
+ };
177
+ const id = subscribe('tis.raw_data_added', onRawAdded);
178
+ return () => { unsubscribe(id); };
179
+ }, [enabled, projectId, methodId, runId, blobName, listCycles, subscribe, unsubscribe]);
180
+
181
+ // Lazy blob fetch — runs whenever identifiers / selectedCycle change.
182
+ // Suppressed entirely when no cycle is selected (e.g. test just
183
+ // started, no raw_data on disk yet): no point asking the server
184
+ // for a file that's known to not exist, and surfacing the resulting
185
+ // error would flash an unfriendly overlay over an otherwise valid
186
+ // "waiting for data" state.
187
+ useEffect(() => {
188
+ if (!enabled || !projectId || !methodId || !runId) {
189
+ setRaw(null); setEnvelope(null); setLoading(false); setError(null);
190
+ return;
191
+ }
192
+ if (selectedCycle == null) {
193
+ setRaw(null); setEnvelope(null); setLoading(false); setError(null);
194
+ return;
195
+ }
196
+ let cancelled = false;
197
+ setLoading(true);
198
+ setError(null);
199
+ (async () => {
200
+ try {
201
+ const resp: any = await invoke(
202
+ 'tis.read_raw' as any, MessageType.Request as any,
203
+ {
204
+ project_id: projectId, method_id: methodId,
205
+ run_id: runId, name: blobName,
206
+ cycle_index: selectedCycle,
207
+ } as any,
208
+ );
209
+ if (cancelled) return;
210
+ if (resp?.success) {
211
+ const payload = resp.data ?? {};
212
+ setEnvelope(payload);
213
+ setRaw(unwrapEnvelope(payload));
214
+ } else {
215
+ setError(resp?.error_message ?? 'Failed to read raw data');
216
+ }
217
+ } catch (e: any) {
218
+ if (!cancelled) setError(String(e?.message ?? e));
219
+ } finally {
220
+ if (!cancelled) setLoading(false);
221
+ }
222
+ })();
223
+ return () => { cancelled = true; };
224
+ }, [enabled, projectId, methodId, runId, blobName, selectedCycle, invoke]);
225
+
226
+ // Public setter wraps setSelectedCycle and flips userPinnedRef so
227
+ // an operator's manual pick freezes the picker for the rest of the
228
+ // run. Re-mounting (new run) clears userPinnedRef in the
229
+ // identifier-change effect above.
230
+ const pickCycle = useCallback((n: number | null) => {
231
+ userPinnedRef.current = n != null;
232
+ setSelectedCycle(n);
233
+ }, []);
234
+
235
+ return {
236
+ cycles, selectedCycle, setSelectedCycle: pickCycle,
237
+ raw, envelope, loading, error,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * `tis.read_raw` returns one of:
243
+ * - per-cycle envelope: `{ cycle_index, cycle_fields, context, data: { col: number[] } }`
244
+ * - legacy flat blob: `{ col: number[] }`
245
+ *
246
+ * Chart and CSV code only care about the columnar payload — peel off
247
+ * the envelope when present, otherwise pass the blob through. Returned
248
+ * shape is always `Record<string, number[]>` so consumers can index
249
+ * uniformly.
250
+ */
251
+ export function unwrapEnvelope(blob: any): Record<string, number[]> {
252
+ if (!blob || typeof blob !== 'object') return {};
253
+ if ('data' in blob && blob.data && typeof blob.data === 'object'
254
+ && Object.values(blob.data).some(v => Array.isArray(v))) {
255
+ return blob.data;
256
+ }
257
+ return blob;
258
+ }