@adcops/autocore-react 3.3.83 → 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.
- package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
- package/dist/components/ams/AssetDetailView.js +1 -1
- package/dist/components/ams/AssetEditDialog.d.ts +13 -0
- package/dist/components/ams/AssetEditDialog.d.ts.map +1 -0
- package/dist/components/ams/AssetEditDialog.js +1 -0
- package/dist/components/ams/index.d.ts +1 -0
- package/dist/components/ams/index.d.ts.map +1 -1
- package/dist/components/ams/index.js +1 -1
- package/dist/components/network/NetworkPanel.d.ts.map +1 -1
- package/dist/components/network/NetworkPanel.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +5 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +15 -1
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/useRawCycleData.d.ts.map +1 -1
- package/dist/components/tis/useRawCycleData.js +1 -1
- package/package.json +1 -1
- package/src/components/ams/AssetDetailView.tsx +31 -0
- package/src/components/ams/AssetEditDialog.tsx +463 -0
- package/src/components/ams/index.ts +1 -0
- package/src/components/network/NetworkPanel.tsx +13 -1
- package/src/components/tis/TestDataView.tsx +80 -16
- package/src/components/tis/TestSetupForm.tsx +60 -6
- package/src/components/tis/useRawCycleData.ts +132 -31
|
@@ -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
|
-
|
|
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!,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
/>
|
|
@@ -11,12 +11,27 @@
|
|
|
11
11
|
* (set `enabled` to false to short-circuit while
|
|
12
12
|
* a cycle_scatter view is active).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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.
|
|
17
32
|
*/
|
|
18
33
|
|
|
19
|
-
import { useContext, useEffect, useState } from 'react';
|
|
34
|
+
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
20
35
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
21
36
|
import { MessageType } from '../../hub/CommandMessage';
|
|
22
37
|
|
|
@@ -50,7 +65,7 @@ export interface UseRawCycleDataResult {
|
|
|
50
65
|
|
|
51
66
|
export function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataResult {
|
|
52
67
|
const { projectId, methodId, runId, blobName, enabled } = opts;
|
|
53
|
-
const { invoke } = useContext(EventEmitterContext);
|
|
68
|
+
const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
|
|
54
69
|
|
|
55
70
|
const [cycles, setCycles] = useState<number[]>([]);
|
|
56
71
|
const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
|
|
@@ -59,14 +74,49 @@ export function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataRe
|
|
|
59
74
|
const [loading, setLoading] = useState(false);
|
|
60
75
|
const [error, setError] = useState<string | null>(null);
|
|
61
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
|
+
|
|
62
109
|
// Reset cycle state when the run identity (or blob) changes — the
|
|
63
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.
|
|
64
113
|
useEffect(() => {
|
|
65
114
|
setCycles([]);
|
|
66
115
|
setSelectedCycle(null);
|
|
67
116
|
setRaw(null);
|
|
68
117
|
setEnvelope(null);
|
|
69
118
|
setError(null);
|
|
119
|
+
userPinnedRef.current = false;
|
|
70
120
|
}, [projectId, methodId, runId, blobName]);
|
|
71
121
|
|
|
72
122
|
// Cycle-list discovery. Runs ahead of the data fetch so the cycle
|
|
@@ -75,47 +125,86 @@ export function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataRe
|
|
|
75
125
|
if (!enabled || !projectId || !methodId || !runId) return;
|
|
76
126
|
let cancelled = false;
|
|
77
127
|
(async () => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
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]);
|
|
96
133
|
}
|
|
97
134
|
})();
|
|
98
135
|
return () => { cancelled = true; };
|
|
99
|
-
}, [enabled, projectId, methodId, runId, blobName,
|
|
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]);
|
|
100
180
|
|
|
101
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.
|
|
102
187
|
useEffect(() => {
|
|
103
188
|
if (!enabled || !projectId || !methodId || !runId) {
|
|
104
189
|
setRaw(null); setEnvelope(null); setLoading(false); setError(null);
|
|
105
190
|
return;
|
|
106
191
|
}
|
|
192
|
+
if (selectedCycle == null) {
|
|
193
|
+
setRaw(null); setEnvelope(null); setLoading(false); setError(null);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
107
196
|
let cancelled = false;
|
|
108
197
|
setLoading(true);
|
|
109
198
|
setError(null);
|
|
110
199
|
(async () => {
|
|
111
200
|
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
201
|
const resp: any = await invoke(
|
|
118
|
-
'tis.read_raw' as any, MessageType.Request as any,
|
|
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,
|
|
119
208
|
);
|
|
120
209
|
if (cancelled) return;
|
|
121
210
|
if (resp?.success) {
|
|
@@ -134,7 +223,19 @@ export function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataRe
|
|
|
134
223
|
return () => { cancelled = true; };
|
|
135
224
|
}, [enabled, projectId, methodId, runId, blobName, selectedCycle, invoke]);
|
|
136
225
|
|
|
137
|
-
|
|
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
|
+
};
|
|
138
239
|
}
|
|
139
240
|
|
|
140
241
|
/**
|