@adcops/autocore-react 3.3.73 → 3.3.77
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/assets/HomeMotor.d.ts +4 -0
- package/dist/assets/HomeMotor.d.ts.map +1 -0
- package/dist/assets/HomeMotor.js +1 -0
- package/dist/assets/svg/home_motor.svg +57 -0
- package/dist/components/Indicator.d.ts +29 -52
- package/dist/components/Indicator.d.ts.map +1 -1
- package/dist/components/Indicator.js +1 -1
- package/dist/components/ValueInput.d.ts +1 -1
- package/dist/components/ValueInput.d.ts.map +1 -1
- package/dist/components/ams/AmsProvider.d.ts +7 -0
- package/dist/components/ams/AmsProvider.d.ts.map +1 -1
- package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
- package/dist/components/ams/AssetDetailView.js +1 -1
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
- package/dist/components/ams/AssetRegistryTable.js +1 -1
- package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -1
- package/dist/components/ams/CalibrationEntryDialog.js +1 -1
- package/dist/components/ams/MissingAssetsBanner.d.ts +11 -0
- package/dist/components/ams/MissingAssetsBanner.d.ts.map +1 -0
- package/dist/components/ams/MissingAssetsBanner.js +1 -0
- package/dist/components/ams/PlaceholderHealthPanel.d.ts +3 -0
- package/dist/components/ams/PlaceholderHealthPanel.d.ts.map +1 -0
- package/dist/components/ams/PlaceholderHealthPanel.js +1 -0
- package/dist/components/ams/index.d.ts +2 -0
- package/dist/components/ams/index.d.ts.map +1 -1
- package/dist/components/ams/index.js +1 -1
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/network/NetworkPanel.d.ts +8 -0
- package/dist/components/network/NetworkPanel.d.ts.map +1 -0
- package/dist/components/network/NetworkPanel.js +1 -0
- package/dist/components/network/NetworkProvider.d.ts +72 -0
- package/dist/components/network/NetworkProvider.d.ts.map +1 -0
- package/dist/components/network/NetworkProvider.js +1 -0
- package/dist/components/network/StagedChangeBanner.d.ts +8 -0
- package/dist/components/network/StagedChangeBanner.d.ts.map +1 -0
- package/dist/components/network/StagedChangeBanner.js +1 -0
- package/dist/components/network/index.d.ts +7 -0
- package/dist/components/network/index.d.ts.map +1 -0
- package/dist/components/network/index.js +1 -0
- package/dist/components/tis/ProjectManager.d.ts +7 -0
- package/dist/components/tis/ProjectManager.d.ts.map +1 -0
- package/dist/components/tis/ProjectManager.js +1 -0
- package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
- package/dist/components/tis/TestRawDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +7 -0
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/package.json +5 -1
- package/src/assets/HomeMotor.tsx +37 -0
- package/src/assets/svg/home_motor.svg +57 -0
- package/src/components/Indicator.tsx +166 -162
- package/src/components/ValueInput.tsx +2 -2
- package/src/components/ams/AmsProvider.tsx +7 -0
- package/src/components/ams/AssetDetailView.tsx +287 -4
- package/src/components/ams/AssetRegistryTable.tsx +325 -21
- package/src/components/ams/CalibrationEntryDialog.tsx +163 -30
- package/src/components/ams/MissingAssetsBanner.tsx +124 -0
- package/src/components/ams/PlaceholderHealthPanel.tsx +188 -0
- package/src/components/ams/index.ts +2 -0
- package/src/components/index.ts +26 -0
- package/src/components/network/NetworkPanel.tsx +363 -0
- package/src/components/network/NetworkProvider.tsx +349 -0
- package/src/components/network/StagedChangeBanner.tsx +101 -0
- package/src/components/network/index.ts +17 -0
- package/src/components/tis/ProjectManager.tsx +392 -0
- package/src/components/tis/ResultHistoryTable.tsx +125 -74
- package/src/components/tis/TestDataView.tsx +160 -14
- package/src/components/tis/TestRawDataView.tsx +118 -8
- package/src/components/tis/TestSetupForm.tsx +42 -1
|
@@ -109,7 +109,12 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
109
109
|
|
|
110
110
|
// Lazy-loaded blobs for the View Raw Data dialog. Fetched only
|
|
111
111
|
// when the dialog opens, and re-fetched if the operator pins a
|
|
112
|
-
// different run while the dialog is closed.
|
|
112
|
+
// different run / cycle while the dialog is closed.
|
|
113
|
+
//
|
|
114
|
+
// Wire format (post per-cycle change) is an envelope:
|
|
115
|
+
// { cycle_index, cycle_fields, context, data: { col: number[] } }
|
|
116
|
+
// The legacy flat `{ col: number[] }` shape is still tolerated for
|
|
117
|
+
// runs written before the change — see unwrapEnvelope() below.
|
|
113
118
|
const [rawBlob, setRawBlob] = useState<any | null>(null);
|
|
114
119
|
const [rawError, setRawError] = useState<string | null>(null);
|
|
115
120
|
const [rawLoading, setRawLoading] = useState(false);
|
|
@@ -117,6 +122,12 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
117
122
|
const [filteredError, setFilteredError] = useState<string | null>(null);
|
|
118
123
|
const [filteredLoading, setFilteredLoading] = useState(false);
|
|
119
124
|
|
|
125
|
+
// Per-test cycle listing for the raw dialog's cycle picker. Loaded
|
|
126
|
+
// once when the dialog opens. selectedCycle is null until the list
|
|
127
|
+
// arrives; we default it to the latest cycle.
|
|
128
|
+
const [availableCycles, setAvailableCycles] = useState<number[]>([]);
|
|
129
|
+
const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
|
|
130
|
+
|
|
120
131
|
// Scatter-capable views only — raw_trace lives in <TestRawDataView>.
|
|
121
132
|
const scatterViews = useMemo(() => {
|
|
122
133
|
const out: { name: string; view: ChartView }[] = [];
|
|
@@ -295,14 +306,42 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
295
306
|
// download in <ResultHistoryTable> pulls.
|
|
296
307
|
// -----------------------------------------------------------------
|
|
297
308
|
const blobName = schema?.raw_data?.blob_name ?? 'trace';
|
|
298
|
-
const fetchKeyRef = useRef<string>(''); // last (project, method, run, blob) we fetched
|
|
309
|
+
const fetchKeyRef = useRef<string>(''); // last (project, method, run, blob, cycle) we fetched
|
|
310
|
+
|
|
311
|
+
// Discover which cycles have raw_data on disk so we can present a
|
|
312
|
+
// picker. tis.list_raw returns the flat name list (back-compat) AND
|
|
313
|
+
// a per-cycle list; we want the cycles for the selected blob name.
|
|
314
|
+
const loadCycleList = useCallback(async (): Promise<number[]> => {
|
|
315
|
+
if (!projectId || !methodId || !runId) return [];
|
|
316
|
+
try {
|
|
317
|
+
const resp: any = await invoke(
|
|
318
|
+
'tis.list_raw' as any, MessageType.Request,
|
|
319
|
+
{ project_id: projectId, method_id: methodId, run_id: runId } as any,
|
|
320
|
+
);
|
|
321
|
+
if (!resp?.success) return [];
|
|
322
|
+
const cycles: any[] = resp.data?.cycles ?? [];
|
|
323
|
+
const indices = cycles
|
|
324
|
+
.filter(c => c?.name === blobName && typeof c?.cycle_index === 'number')
|
|
325
|
+
.map(c => c.cycle_index as number)
|
|
326
|
+
.sort((a, b) => a - b);
|
|
327
|
+
return indices;
|
|
328
|
+
} catch {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
}, [projectId, methodId, runId, blobName, invoke]);
|
|
299
332
|
|
|
300
|
-
const loadBlobs = useCallback(async () => {
|
|
333
|
+
const loadBlobs = useCallback(async (cycleIndex: number | null) => {
|
|
301
334
|
if (!projectId || !methodId || !runId) return;
|
|
302
|
-
const key = `${projectId}|${methodId}|${runId}|${blobName}`;
|
|
303
|
-
if (fetchKeyRef.current === key) return; // already loaded for this
|
|
335
|
+
const key = `${projectId}|${methodId}|${runId}|${blobName}|${cycleIndex ?? 'latest'}`;
|
|
336
|
+
if (fetchKeyRef.current === key) return; // already loaded for this slice
|
|
304
337
|
fetchKeyRef.current = key;
|
|
305
338
|
|
|
339
|
+
const baseArgs: Record<string, any> = {
|
|
340
|
+
project_id: projectId, method_id: methodId,
|
|
341
|
+
run_id: runId, name: blobName,
|
|
342
|
+
};
|
|
343
|
+
if (cycleIndex != null) baseArgs.cycle_index = cycleIndex;
|
|
344
|
+
|
|
306
345
|
// Raw — must succeed for the dialog to be useful, but a
|
|
307
346
|
// missing file is logged and surfaced rather than aborted.
|
|
308
347
|
setRawLoading(true);
|
|
@@ -310,8 +349,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
310
349
|
setRawBlob(null);
|
|
311
350
|
try {
|
|
312
351
|
const resp: any = await invoke(
|
|
313
|
-
'tis.read_raw' as any, MessageType.Request,
|
|
314
|
-
{ project_id: projectId, method_id: methodId, run_id: runId, name: blobName } as any,
|
|
352
|
+
'tis.read_raw' as any, MessageType.Request, baseArgs as any,
|
|
315
353
|
);
|
|
316
354
|
if (resp?.success) {
|
|
317
355
|
setRawBlob(resp.data ?? {});
|
|
@@ -327,6 +365,8 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
327
365
|
// Filtered is optional. The 'no filtered data' case is the
|
|
328
366
|
// common one (only the post-processing pipeline writes it),
|
|
329
367
|
// and we render a friendly message rather than an error tone.
|
|
368
|
+
// Filtered files are not per-cycle (yet); cycle_index is
|
|
369
|
+
// ignored on the filtered side.
|
|
330
370
|
setFilteredLoading(true);
|
|
331
371
|
setFilteredError(null);
|
|
332
372
|
setFilteredBlob(null);
|
|
@@ -347,18 +387,34 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
347
387
|
}
|
|
348
388
|
}, [projectId, methodId, runId, blobName, invoke]);
|
|
349
389
|
|
|
350
|
-
// When the run changes,
|
|
351
|
-
// open refetches. Don't auto-fetch — that's wasteful if the
|
|
352
|
-
// operator never opens the dialog.
|
|
390
|
+
// When the run / blob changes, reset cycle picker and cache key.
|
|
353
391
|
useEffect(() => {
|
|
354
392
|
fetchKeyRef.current = '';
|
|
393
|
+
setAvailableCycles([]);
|
|
394
|
+
setSelectedCycle(null);
|
|
355
395
|
}, [projectId, methodId, runId, blobName]);
|
|
356
396
|
|
|
357
|
-
const openRawDialog = () => {
|
|
397
|
+
const openRawDialog = async () => {
|
|
358
398
|
setRawOpen(true);
|
|
359
|
-
|
|
399
|
+
// Resolve cycle list lazily — first open after a run change.
|
|
400
|
+
let cycles = availableCycles;
|
|
401
|
+
if (cycles.length === 0) {
|
|
402
|
+
cycles = await loadCycleList();
|
|
403
|
+
setAvailableCycles(cycles);
|
|
404
|
+
}
|
|
405
|
+
const latest = cycles.length > 0 ? cycles[cycles.length - 1] : null;
|
|
406
|
+
const initial = selectedCycle ?? latest;
|
|
407
|
+
if (selectedCycle !== initial) setSelectedCycle(initial);
|
|
408
|
+
await loadBlobs(initial);
|
|
360
409
|
};
|
|
361
410
|
|
|
411
|
+
// Re-fetch when the operator picks a different cycle from the dialog.
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
if (rawOpen && selectedCycle != null) {
|
|
414
|
+
void loadBlobs(selectedCycle);
|
|
415
|
+
}
|
|
416
|
+
}, [rawOpen, selectedCycle, loadBlobs]);
|
|
417
|
+
|
|
362
418
|
// -----------------------------------------------------------------
|
|
363
419
|
// Render
|
|
364
420
|
// -----------------------------------------------------------------
|
|
@@ -436,10 +492,16 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
436
492
|
style={{ width: '90vw', height: '80vh' }}
|
|
437
493
|
maximizable
|
|
438
494
|
>
|
|
495
|
+
<CyclePickerBar
|
|
496
|
+
cycles={availableCycles}
|
|
497
|
+
selected={selectedCycle}
|
|
498
|
+
onChange={setSelectedCycle}
|
|
499
|
+
/>
|
|
500
|
+
<RawEnvelopeHeader envelope={rawBlob} />
|
|
439
501
|
<TabView style={{ height: '100%' }}>
|
|
440
502
|
<TabPanel header="Raw Data">
|
|
441
503
|
<DataBlobTable
|
|
442
|
-
blob={rawBlob}
|
|
504
|
+
blob={unwrapEnvelope(rawBlob)}
|
|
443
505
|
loading={rawLoading}
|
|
444
506
|
error={rawError}
|
|
445
507
|
rawData={schema.raw_data}
|
|
@@ -447,7 +509,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
447
509
|
</TabPanel>
|
|
448
510
|
<TabPanel header="Filtered Data">
|
|
449
511
|
<DataBlobTable
|
|
450
|
-
blob={filteredBlob}
|
|
512
|
+
blob={unwrapEnvelope(filteredBlob)}
|
|
451
513
|
loading={filteredLoading}
|
|
452
514
|
error={filteredError}
|
|
453
515
|
rawData={schema.raw_data}
|
|
@@ -567,6 +629,90 @@ const ConfigList: React.FC<{ config: any }> = ({ config }) => {
|
|
|
567
629
|
);
|
|
568
630
|
};
|
|
569
631
|
|
|
632
|
+
// -------------------------------------------------------------------------
|
|
633
|
+
// unwrapEnvelope — `tis.read_raw` returns one of:
|
|
634
|
+
// - the per-cycle envelope `{ cycle_index, cycle_fields, context, data }`
|
|
635
|
+
// - the legacy flat columnar blob `{ col: number[] }` (pre per-cycle)
|
|
636
|
+
// Downstream consumers (DataBlobTable, chart datasets, CSV) only care
|
|
637
|
+
// about the columnar payload — peel off the envelope when present so
|
|
638
|
+
// both shapes render identically.
|
|
639
|
+
// -------------------------------------------------------------------------
|
|
640
|
+
const unwrapEnvelope = (blob: any): any => {
|
|
641
|
+
if (!blob || typeof blob !== 'object') return blob;
|
|
642
|
+
if ('data' in blob && blob.data && typeof blob.data === 'object'
|
|
643
|
+
&& Object.values(blob.data).some(v => Array.isArray(v))) {
|
|
644
|
+
return blob.data;
|
|
645
|
+
}
|
|
646
|
+
return blob;
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
// -------------------------------------------------------------------------
|
|
650
|
+
// CyclePickerBar — dropdown of available cycle indices for the raw_data
|
|
651
|
+
// dialog. Hidden when only one (or zero) cycles exist; rendered as a
|
|
652
|
+
// labelled selector otherwise.
|
|
653
|
+
// -------------------------------------------------------------------------
|
|
654
|
+
const CyclePickerBar: React.FC<{
|
|
655
|
+
cycles: number[];
|
|
656
|
+
selected: number | null;
|
|
657
|
+
onChange: (idx: number) => void;
|
|
658
|
+
}> = ({ cycles, selected, onChange }) => {
|
|
659
|
+
if (cycles.length <= 1) return null;
|
|
660
|
+
return (
|
|
661
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem',
|
|
662
|
+
padding: '0.25rem 0.5rem 0.5rem' }}>
|
|
663
|
+
<label htmlFor="raw-cycle-picker"
|
|
664
|
+
style={{ color: 'var(--text-secondary-color)' }}>Cycle:</label>
|
|
665
|
+
<Dropdown
|
|
666
|
+
inputId="raw-cycle-picker"
|
|
667
|
+
value={selected}
|
|
668
|
+
options={cycles.map(c => ({ label: `Cycle ${c}`, value: c }))}
|
|
669
|
+
onChange={(e) => onChange(Number(e.value))}
|
|
670
|
+
style={{ minWidth: '8rem' }}
|
|
671
|
+
/>
|
|
672
|
+
<span style={{ color: 'var(--text-secondary-color)' }}>
|
|
673
|
+
({cycles.length} cycles recorded)
|
|
674
|
+
</span>
|
|
675
|
+
</div>
|
|
676
|
+
);
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// -------------------------------------------------------------------------
|
|
680
|
+
// RawEnvelopeHeader — small key/value strip showing the metadata embedded
|
|
681
|
+
// in the on-disk envelope (cycle_index, schema-declared cycle_fields,
|
|
682
|
+
// capture context). Renders nothing for legacy flat blobs.
|
|
683
|
+
// -------------------------------------------------------------------------
|
|
684
|
+
const RawEnvelopeHeader: React.FC<{ envelope: any }> = ({ envelope }) => {
|
|
685
|
+
if (!envelope || typeof envelope !== 'object') return null;
|
|
686
|
+
const cycleIndex = envelope.cycle_index;
|
|
687
|
+
const cycleFields = envelope.cycle_fields;
|
|
688
|
+
const context = envelope.context;
|
|
689
|
+
if (cycleIndex == null && !cycleFields && !context) return null;
|
|
690
|
+
|
|
691
|
+
const renderKV = (obj: any) => {
|
|
692
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
693
|
+
const entries = Object.entries(obj).filter(([, v]) =>
|
|
694
|
+
v !== null && typeof v !== 'object');
|
|
695
|
+
if (entries.length === 0) return null;
|
|
696
|
+
return entries.map(([k, v]) => (
|
|
697
|
+
<span key={k} style={{ marginRight: '1rem' }}>
|
|
698
|
+
<span style={{ color: 'var(--text-secondary-color)' }}>{k}: </span>
|
|
699
|
+
<span>{String(v)}</span>
|
|
700
|
+
</span>
|
|
701
|
+
));
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
return (
|
|
705
|
+
<div style={{ padding: '0.5rem', borderBottom: '1px solid var(--surface-border)',
|
|
706
|
+
fontSize: '0.9rem' }}>
|
|
707
|
+
{cycleIndex != null && (
|
|
708
|
+
<div><strong>Cycle {cycleIndex}</strong></div>
|
|
709
|
+
)}
|
|
710
|
+
{cycleFields && <div style={{ marginTop: '0.25rem' }}>{renderKV(cycleFields)}</div>}
|
|
711
|
+
{context && <div style={{ marginTop: '0.25rem' }}>{renderKV(context)}</div>}
|
|
712
|
+
</div>
|
|
713
|
+
);
|
|
714
|
+
};
|
|
715
|
+
|
|
570
716
|
// -------------------------------------------------------------------------
|
|
571
717
|
// DataBlobTable — tabular display of a columnar JSON blob
|
|
572
718
|
// (`{ col_name: number[] }`). Used by the View Raw Data dialog for both
|
|
@@ -56,10 +56,16 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
|
|
|
56
56
|
const { invoke } = useContext(EventEmitterContext);
|
|
57
57
|
|
|
58
58
|
const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
|
|
59
|
+
const [envelope, setEnvelope] = useState<any | null>(null);
|
|
59
60
|
const [loading, setLoading] = useState(true);
|
|
60
61
|
const [error, setError] = useState<string | null>(null);
|
|
61
62
|
const chartRef = useRef<any>(null);
|
|
62
63
|
|
|
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
|
+
|
|
63
69
|
// raw_trace-capable views only — cycle scatter lives in <TestDataView>.
|
|
64
70
|
const traceViews = useMemo(() => {
|
|
65
71
|
const out: { name: string; view: ChartView }[] = [];
|
|
@@ -75,10 +81,45 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
|
|
|
75
81
|
|
|
76
82
|
const effectiveBlobName = blobName ?? schema?.raw_data?.blob_name ?? 'trace';
|
|
77
83
|
|
|
78
|
-
//
|
|
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.
|
|
79
120
|
useEffect(() => {
|
|
80
121
|
if (!projectId || !methodId || !runId) {
|
|
81
|
-
setRaw(null); setLoading(false); setError(null);
|
|
122
|
+
setRaw(null); setEnvelope(null); setLoading(false); setError(null);
|
|
82
123
|
return;
|
|
83
124
|
}
|
|
84
125
|
let cancelled = false;
|
|
@@ -86,13 +127,18 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
|
|
|
86
127
|
setError(null);
|
|
87
128
|
(async () => {
|
|
88
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;
|
|
89
135
|
const resp: any = await invoke(
|
|
90
|
-
'tis.read_raw' as any, MessageType.Request as any,
|
|
91
|
-
{ project_id: projectId, method_id: methodId,
|
|
92
|
-
run_id: runId, name: effectiveBlobName } as any);
|
|
136
|
+
'tis.read_raw' as any, MessageType.Request as any, args as any);
|
|
93
137
|
if (cancelled) return;
|
|
94
138
|
if (resp?.success) {
|
|
95
|
-
|
|
139
|
+
const payload = resp.data ?? {};
|
|
140
|
+
setEnvelope(payload);
|
|
141
|
+
setRaw(unwrapEnvelope(payload));
|
|
96
142
|
} else {
|
|
97
143
|
setError(resp?.error_message ?? 'Failed to read raw data');
|
|
98
144
|
}
|
|
@@ -103,7 +149,7 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
|
|
|
103
149
|
}
|
|
104
150
|
})();
|
|
105
151
|
return () => { cancelled = true; };
|
|
106
|
-
}, [projectId, methodId, runId, effectiveBlobName, invoke]);
|
|
152
|
+
}, [projectId, methodId, runId, effectiveBlobName, selectedCycle, invoke]);
|
|
107
153
|
|
|
108
154
|
const chartData = useMemo(() => {
|
|
109
155
|
if (!raw || !selectedView) return null;
|
|
@@ -174,7 +220,7 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
|
|
|
174
220
|
|
|
175
221
|
return (
|
|
176
222
|
<div className="vblock" style={{ display: 'flex', flexDirection: 'column', gap: '1rem', height: '100%' }}>
|
|
177
|
-
<div className="flex" style={{ gap: '1rem', alignItems: 'center' }}>
|
|
223
|
+
<div className="flex" style={{ gap: '1rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
|
178
224
|
<Dropdown
|
|
179
225
|
value={selectedView}
|
|
180
226
|
options={traceViews.map(v => ({ label: v.view.title ?? v.name, value: v.name }))}
|
|
@@ -182,12 +228,30 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
|
|
|
182
228
|
placeholder="Select a view"
|
|
183
229
|
/>
|
|
184
230
|
<h3 style={{ margin: 0 }}>{selectedViewDef?.title ?? ''}</h3>
|
|
231
|
+
{cycles.length > 1 && (
|
|
232
|
+
<>
|
|
233
|
+
<label htmlFor="rawview-cycle-picker"
|
|
234
|
+
style={{ color: 'var(--text-secondary-color)' }}>Cycle:</label>
|
|
235
|
+
<Dropdown
|
|
236
|
+
inputId="rawview-cycle-picker"
|
|
237
|
+
value={selectedCycle}
|
|
238
|
+
options={cycles.map(c => ({ label: `Cycle ${c}`, value: c }))}
|
|
239
|
+
onChange={(e) => setSelectedCycle(Number(e.value))}
|
|
240
|
+
style={{ minWidth: '8rem' }}
|
|
241
|
+
/>
|
|
242
|
+
<span style={{ color: 'var(--text-secondary-color)' }}>
|
|
243
|
+
of {cycles.length}
|
|
244
|
+
</span>
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
185
247
|
<div style={{ flex: 1 }} />
|
|
186
248
|
<Button icon="pi pi-refresh" label="Reset Zoom"
|
|
187
249
|
outlined
|
|
188
250
|
onClick={() => chartRef.current?.resetZoom?.()} />
|
|
189
251
|
</div>
|
|
190
252
|
|
|
253
|
+
<EnvelopeMetaStrip envelope={envelope} />
|
|
254
|
+
|
|
191
255
|
<div style={{ flex: 1, minHeight: 0, height: chartHeight, position: 'relative' }}>
|
|
192
256
|
{loading && <Overlay>Loading raw data…</Overlay>}
|
|
193
257
|
{error && <Overlay>{error}</Overlay>}
|
|
@@ -215,6 +279,52 @@ const EmptyState: React.FC<{ message: string }> = ({ message }) => (
|
|
|
215
279
|
<div style={{ padding: '1rem', color: 'var(--text-secondary-color)' }}>{message}</div>
|
|
216
280
|
);
|
|
217
281
|
|
|
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
|
+
// Strip of cycle_index + cycle_fields + context, rendered above the
|
|
297
|
+
// chart so the operator can see *which* cycle they're looking at and
|
|
298
|
+
// what schema-declared metric values were active for it. Renders
|
|
299
|
+
// nothing for legacy flat blobs (no envelope to read).
|
|
300
|
+
const EnvelopeMetaStrip: React.FC<{ envelope: any }> = ({ envelope }) => {
|
|
301
|
+
if (!envelope || typeof envelope !== 'object') return null;
|
|
302
|
+
const ci = envelope.cycle_index;
|
|
303
|
+
const cf = envelope.cycle_fields;
|
|
304
|
+
const ctx = envelope.context;
|
|
305
|
+
if (ci == null && !cf && !ctx) return null;
|
|
306
|
+
const kv = (obj: any) => {
|
|
307
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
308
|
+
return Object.entries(obj)
|
|
309
|
+
.filter(([, v]) => v !== null && typeof v !== 'object')
|
|
310
|
+
.map(([k, v]) => (
|
|
311
|
+
<span key={k} style={{ marginRight: '1rem' }}>
|
|
312
|
+
<span style={{ color: 'var(--text-secondary-color)' }}>{k}: </span>
|
|
313
|
+
<span>{String(v)}</span>
|
|
314
|
+
</span>
|
|
315
|
+
));
|
|
316
|
+
};
|
|
317
|
+
return (
|
|
318
|
+
<div style={{ padding: '0.5rem 0.25rem', fontSize: '0.9rem',
|
|
319
|
+
borderTop: '1px solid var(--surface-border)',
|
|
320
|
+
borderBottom: '1px solid var(--surface-border)' }}>
|
|
321
|
+
{ci != null && <div><strong>Cycle {ci}</strong></div>}
|
|
322
|
+
{cf && <div style={{ marginTop: '0.25rem' }}>{kv(cf)}</div>}
|
|
323
|
+
{ctx && <div style={{ marginTop: '0.25rem' }}>{kv(ctx)}</div>}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
218
328
|
const CHART_COLORS = [
|
|
219
329
|
'#4ea8de', '#f59e0b', '#22c55e', '#a855f7',
|
|
220
330
|
'#ef4444', '#14b8a6', '#eab308', '#ec4899',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
|
|
2
2
|
import { Button } from 'primereact/button';
|
|
3
3
|
import { InputText } from 'primereact/inputtext';
|
|
4
4
|
import { Dropdown } from 'primereact/dropdown';
|
|
@@ -38,6 +38,13 @@ export interface TestFieldDef {
|
|
|
38
38
|
label?: string;
|
|
39
39
|
/** Long-form guidance surfaced as a hover tooltip on an info icon. */
|
|
40
40
|
description?: string;
|
|
41
|
+
/** Seed value applied when the operator selects this test method.
|
|
42
|
+
* For source-bound fields the default is written to the GM tag so
|
|
43
|
+
* the control program sees it; for non-source fields it's stashed
|
|
44
|
+
* straight into stagedConfig. Operator edits override per-stage,
|
|
45
|
+
* but the schema default never mutates — re-selecting the method
|
|
46
|
+
* re-applies the default. */
|
|
47
|
+
default?: any;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
export interface TestMethod {
|
|
@@ -240,6 +247,40 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
240
247
|
);
|
|
241
248
|
const closeInfoDialog = () => setInfoDialog({ open: false, title: '', body: null });
|
|
242
249
|
|
|
250
|
+
// Apply schema-declared defaults when the operator picks a method.
|
|
251
|
+
// Source-bound fields write the default to GM (the control program
|
|
252
|
+
// is the consumer of record); non-source fields land directly in
|
|
253
|
+
// stagedConfig. We track the last method we seeded so subsequent
|
|
254
|
+
// re-renders don't clobber operator edits — only an actual method
|
|
255
|
+
// change re-applies the defaults.
|
|
256
|
+
const defaultsAppliedFor = useRef<string>('');
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (!schema || !methodId) return;
|
|
259
|
+
if (defaultsAppliedFor.current === methodId) return;
|
|
260
|
+
defaultsAppliedFor.current = methodId;
|
|
261
|
+
|
|
262
|
+
setConfig((prev: any) => {
|
|
263
|
+
let next = prev;
|
|
264
|
+
for (const field of schema.config_fields) {
|
|
265
|
+
if (field.name === 'sample_id') continue;
|
|
266
|
+
if (field.default === undefined || field.default === null) continue;
|
|
267
|
+
if (next === prev) next = { ...prev };
|
|
268
|
+
next[field.name] = field.default;
|
|
269
|
+
if (field.source) {
|
|
270
|
+
// Mirror handleFieldChange: write to GM so the
|
|
271
|
+
// control program sees the default. Errors here are
|
|
272
|
+
// logged but non-fatal — the form still reflects
|
|
273
|
+
// the default locally.
|
|
274
|
+
void Promise.resolve()
|
|
275
|
+
.then(() => write(field.source!, field.default))
|
|
276
|
+
.catch(e => console.error(
|
|
277
|
+
`[TestSetupForm] Failed to seed default for ${field.name}:`, e));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return next;
|
|
281
|
+
});
|
|
282
|
+
}, [schema, methodId, write]);
|
|
283
|
+
|
|
243
284
|
// Seed and live-update config_fields that declare a `source`.
|
|
244
285
|
useEffect(() => {
|
|
245
286
|
if (!schema) return;
|