@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.
- 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 +8 -0
- 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 +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 +39 -0
- package/dist/components/tis/useRawCycleData.d.ts.map +1 -0
- package/dist/components/tis/useRawCycleData.js +1 -0
- 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 +256 -84
- package/src/components/tis/TestRawDataView.tsx +15 -97
- package/src/components/tis/TestSetupForm.tsx +60 -6
- package/src/components/tis/useRawCycleData.ts +258 -0
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
3
|
*
|
|
4
4
|
* TestDataView — standardized test-detail view for the Test Information
|
|
5
|
-
* System. Renders
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* System. Renders, top-to-bottom:
|
|
6
|
+
*
|
|
7
|
+
* - metadata header (sample, project/method/run, "View Raw Data" btn)
|
|
8
|
+
* - **unified chart panel**: one dropdown lists every view declared in
|
|
9
|
+
* the schema (any type). The chart area dispatches on `view.type`:
|
|
10
|
+
* * `raw_trace` — plots columns from the per-cycle raw blob,
|
|
11
|
+
* shows a cycle picker when >1 cycle exists.
|
|
12
|
+
* * `cycle_scatter` — plots per-cycle scalars across the full run.
|
|
13
|
+
* Raw blob fetching is lazy: nothing's pulled until a raw_trace view
|
|
14
|
+
* is actually selected, so scatter-only runs don't pay the round trip.
|
|
15
|
+
* - virtual-scroll cycle table
|
|
16
|
+
* - results table
|
|
17
|
+
*
|
|
18
|
+
* Subscribes to live `tis.cycle_added` and `tis.results_updated`
|
|
19
|
+
* broadcasts so cycle data + scatter chart update as the control
|
|
20
|
+
* program appends cycles.
|
|
21
|
+
*
|
|
22
|
+
* Sibling <TestRawDataView> stays exported for callers that want a
|
|
23
|
+
* focused trace-only viewer with no scatter / cycle-table / results
|
|
24
|
+
* sections.
|
|
9
25
|
*/
|
|
10
26
|
|
|
11
27
|
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
@@ -26,6 +42,7 @@ import { Line } from 'react-chartjs-2';
|
|
|
26
42
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
27
43
|
import { MessageType } from '../../hub/CommandMessage';
|
|
28
44
|
import { useTis } from './TisProvider';
|
|
45
|
+
import { useRawCycleData } from './useRawCycleData';
|
|
29
46
|
|
|
30
47
|
ChartJS.register(
|
|
31
48
|
CategoryScale, LinearScale, PointElement, LineElement,
|
|
@@ -43,6 +60,11 @@ export interface TestFieldDef {
|
|
|
43
60
|
units?: string;
|
|
44
61
|
required?: boolean;
|
|
45
62
|
source?: string;
|
|
63
|
+
/** Optional display-time scale multiplier. `display = raw * scale`,
|
|
64
|
+
* default 1.0 = no conversion. Cycle Data and Results panels
|
|
65
|
+
* apply this when formatting numeric cells. Charts plot raw
|
|
66
|
+
* values (axis labels already carry units). Storage stays raw. */
|
|
67
|
+
scale?: number;
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
export interface ChartAxis { field?: string; column?: string; label?: string; }
|
|
@@ -88,6 +110,9 @@ export interface TestDataViewProps {
|
|
|
88
110
|
throttleMs?: number;
|
|
89
111
|
/** Fixed cycle-table scroll height. Default "400px". */
|
|
90
112
|
cycleTableHeight?: string;
|
|
113
|
+
/** Height of the unified chart panel (any CSS length). Default "320px".
|
|
114
|
+
* Set to e.g. "50vh" for a taller chart on a single-test page. */
|
|
115
|
+
chartHeight?: string;
|
|
91
116
|
}
|
|
92
117
|
|
|
93
118
|
// -------------------------------------------------------------------------
|
|
@@ -98,7 +123,7 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
98
123
|
const methodId = props.methodId ?? tis.selection.methodId;
|
|
99
124
|
const runId = props.runId ?? tis.selection.runId;
|
|
100
125
|
const schema = props.schema ?? (methodId ? (tis.schemas[methodId] as TestMethod) : undefined);
|
|
101
|
-
const { throttleMs = 100, cycleTableHeight = '400px' } = props;
|
|
126
|
+
const { throttleMs = 100, cycleTableHeight = '400px', chartHeight = '320px' } = props;
|
|
102
127
|
const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
|
|
103
128
|
|
|
104
129
|
const [meta, setMeta] = useState<any>(null);
|
|
@@ -107,6 +132,13 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
107
132
|
const [rawOpen, setRawOpen] = useState(false);
|
|
108
133
|
const [configOpen, setConfigOpen] = useState(false);
|
|
109
134
|
|
|
135
|
+
// Direct handle on the chart.js instance so the toolbar's reset-
|
|
136
|
+
// zoom button can call chart.resetZoom() — the zoom plugin's only
|
|
137
|
+
// imperative API. Customers like the wheel/pinch/drag zoom but
|
|
138
|
+
// can get lost in the chart with no obvious way back; the icon
|
|
139
|
+
// button in the chart's header row is the escape hatch.
|
|
140
|
+
const chartRef = useRef<any>(null);
|
|
141
|
+
|
|
110
142
|
// Lazy-loaded blobs for the View Raw Data dialog. Fetched only
|
|
111
143
|
// when the dialog opens, and re-fetched if the operator pins a
|
|
112
144
|
// different run / cycle while the dialog is closed.
|
|
@@ -128,37 +160,42 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
128
160
|
const [availableCycles, setAvailableCycles] = useState<number[]>([]);
|
|
129
161
|
const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
|
|
130
162
|
|
|
131
|
-
//
|
|
132
|
-
|
|
163
|
+
// All views, any type, in declaration order. The dropdown lists
|
|
164
|
+
// every one; the chart area dispatches on `view.type` to render
|
|
165
|
+
// either a scatter or a raw_trace chart from the appropriate data.
|
|
166
|
+
const allViews = useMemo(() => {
|
|
133
167
|
const out: { name: string; view: ChartView }[] = [];
|
|
134
168
|
for (const [name, v] of Object.entries(schema?.views ?? {})) {
|
|
135
|
-
|
|
169
|
+
out.push({ name, view: v as ChartView });
|
|
136
170
|
}
|
|
137
171
|
return out;
|
|
138
172
|
}, [schema]);
|
|
139
173
|
|
|
140
174
|
const [selectedView, setSelectedView] = useState<string | null>(
|
|
141
|
-
|
|
175
|
+
allViews.length > 0 ? allViews[0].name : null,
|
|
142
176
|
);
|
|
143
177
|
|
|
144
178
|
// Default to the first available view as soon as the schema loads.
|
|
145
179
|
// The useState initializer above only runs on first mount, when
|
|
146
|
-
//
|
|
180
|
+
// allViews is typically still empty (schema fetch in flight).
|
|
147
181
|
// Without this effect, selectedView stays null and the chart
|
|
148
182
|
// stays blank until the operator opens the dropdown — almost
|
|
149
183
|
// nobody does, so they email asking why the chart is broken.
|
|
150
184
|
//
|
|
151
185
|
// Also handles the case where the schema changes and the
|
|
152
|
-
// currently-selected view is no longer in
|
|
153
|
-
//
|
|
186
|
+
// currently-selected view is no longer in allViews — falls back
|
|
187
|
+
// to the new first view rather than rendering nothing.
|
|
154
188
|
useEffect(() => {
|
|
155
|
-
if (
|
|
189
|
+
if (allViews.length === 0) return;
|
|
156
190
|
const stillValid = selectedView !== null
|
|
157
|
-
&&
|
|
191
|
+
&& allViews.some(v => v.name === selectedView);
|
|
158
192
|
if (!stillValid) {
|
|
159
|
-
setSelectedView(
|
|
193
|
+
setSelectedView(allViews[0].name);
|
|
160
194
|
}
|
|
161
|
-
}, [
|
|
195
|
+
}, [allViews, selectedView]);
|
|
196
|
+
|
|
197
|
+
const selectedViewDef = allViews.find(v => v.name === selectedView)?.view;
|
|
198
|
+
const isRawTraceView = selectedViewDef?.type === 'raw_trace';
|
|
162
199
|
|
|
163
200
|
// Pending updates coalesced by a throttle window — keeps React
|
|
164
201
|
// re-renders at <= 1 / throttleMs even if cycles stream faster.
|
|
@@ -245,58 +282,102 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
245
282
|
}, [projectId, methodId, runId, throttleMs]);
|
|
246
283
|
|
|
247
284
|
// -----------------------------------------------------------------
|
|
248
|
-
//
|
|
285
|
+
// Raw-trace data fetch (lazy)
|
|
286
|
+
//
|
|
287
|
+
// Only ever pulls a blob when the active chart view is a raw_trace.
|
|
288
|
+
// Switching to a cycle_scatter view leaves the previously-loaded
|
|
289
|
+
// raw state alone (still in memory but unused), so flipping back
|
|
290
|
+
// and forth is instant after the first fetch.
|
|
291
|
+
// -----------------------------------------------------------------
|
|
292
|
+
const traceBlobName = schema?.raw_data?.blob_name ?? 'trace';
|
|
293
|
+
const traceFetch = useRawCycleData({
|
|
294
|
+
projectId, methodId, runId,
|
|
295
|
+
blobName: traceBlobName,
|
|
296
|
+
enabled: isRawTraceView,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// -----------------------------------------------------------------
|
|
300
|
+
// Chart data — dispatches on view.type so one panel handles both
|
|
301
|
+
// shapes. Returns null when the active view's input data isn't
|
|
302
|
+
// ready (e.g., raw blob still loading); the render block treats
|
|
303
|
+
// null as "show overlay instead of an empty chart."
|
|
249
304
|
// -----------------------------------------------------------------
|
|
250
305
|
const chartData = useMemo(() => {
|
|
251
|
-
if (!
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
306
|
+
if (!selectedViewDef) return null;
|
|
307
|
+
if (selectedViewDef.type === 'cycle_scatter') {
|
|
308
|
+
const xField = selectedViewDef.x.field;
|
|
309
|
+
if (!xField) return null;
|
|
310
|
+
const asc = [...cycles].reverse(); // state is newest-first; charts want oldest-first
|
|
311
|
+
const xs = asc.map(c => c[xField]);
|
|
312
|
+
const datasets = selectedViewDef.y.map((s, idx) => ({
|
|
313
|
+
label: s.label ?? s.field,
|
|
314
|
+
data: asc.map(c => c[s.field!]),
|
|
315
|
+
yAxisID: s.y_axis === 'right' ? 'y1' : 'y',
|
|
316
|
+
borderColor: palette(idx),
|
|
317
|
+
backgroundColor: palette(idx),
|
|
318
|
+
tension: 0.1,
|
|
319
|
+
pointRadius: 2,
|
|
320
|
+
}));
|
|
321
|
+
return { labels: xs, datasets };
|
|
322
|
+
}
|
|
323
|
+
if (selectedViewDef.type === 'raw_trace') {
|
|
324
|
+
if (!traceFetch.raw) return null;
|
|
325
|
+
const xCol = selectedViewDef.x.column;
|
|
326
|
+
if (!xCol) return null;
|
|
327
|
+
const xs = traceFetch.raw[xCol] ?? [];
|
|
328
|
+
const datasets = selectedViewDef.y.map((s, idx) => ({
|
|
329
|
+
label: s.label ?? s.column,
|
|
330
|
+
data: (traceFetch.raw![s.column!] ?? []).map((y, i) => ({ x: xs[i], y })),
|
|
331
|
+
yAxisID: s.y_axis === 'right' ? 'y1' : 'y',
|
|
332
|
+
borderColor: palette(idx),
|
|
333
|
+
backgroundColor: palette(idx),
|
|
334
|
+
pointRadius: 0,
|
|
335
|
+
borderWidth: 1.5,
|
|
336
|
+
showLine: true,
|
|
337
|
+
}));
|
|
338
|
+
return { datasets };
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}, [selectedViewDef, cycles, traceFetch.raw]);
|
|
342
|
+
|
|
273
343
|
const usesRightAxis = selectedViewDef?.y.some(s => s.y_axis === 'right') ?? false;
|
|
274
344
|
|
|
275
|
-
const chartOptions = useMemo(() =>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
345
|
+
const chartOptions = useMemo(() => {
|
|
346
|
+
const isTrace = selectedViewDef?.type === 'raw_trace';
|
|
347
|
+
return {
|
|
348
|
+
responsive: true,
|
|
349
|
+
maintainAspectRatio: false,
|
|
350
|
+
// raw_trace datasets are pre-built `{x, y}` points so
|
|
351
|
+
// chart.js shouldn't try to parse them; cycle_scatter uses
|
|
352
|
+
// the `labels` + per-dataset `data: number[]` shape and
|
|
353
|
+
// needs default parsing on.
|
|
354
|
+
parsing: isTrace ? (false as const) : undefined,
|
|
355
|
+
scales: {
|
|
356
|
+
x: isTrace
|
|
357
|
+
? { type: 'linear' as const,
|
|
358
|
+
title: { display: !!selectedViewDef?.x.label, text: selectedViewDef?.x.label } }
|
|
359
|
+
: { title: { display: !!selectedViewDef?.x.label, text: selectedViewDef?.x.label } },
|
|
360
|
+
y: { position: 'left' as const,
|
|
361
|
+
title: { display: true, text: leftAxisLabel(selectedViewDef) } },
|
|
362
|
+
...(usesRightAxis ? {
|
|
363
|
+
y1: { position: 'right' as const,
|
|
364
|
+
grid: { drawOnChartArea: false },
|
|
365
|
+
title: { display: true, text: rightAxisLabel(selectedViewDef) } },
|
|
366
|
+
} : {}),
|
|
367
|
+
},
|
|
368
|
+
plugins: {
|
|
369
|
+
legend: { display: true },
|
|
292
370
|
zoom: {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
371
|
+
pan: { enabled: true, mode: 'xy' as const },
|
|
372
|
+
zoom: {
|
|
373
|
+
wheel: { enabled: true },
|
|
374
|
+
pinch: { enabled: true },
|
|
375
|
+
mode: 'xy' as const,
|
|
376
|
+
},
|
|
296
377
|
},
|
|
297
378
|
},
|
|
298
|
-
}
|
|
299
|
-
}
|
|
379
|
+
};
|
|
380
|
+
}, [selectedViewDef, usesRightAxis]);
|
|
300
381
|
|
|
301
382
|
// -----------------------------------------------------------------
|
|
302
383
|
// View Raw Data dialog: lazy-fetch raw + filtered blobs the first
|
|
@@ -439,36 +520,92 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
439
520
|
affordance instead of silence. The dropdown is disabled
|
|
440
521
|
in that case; the chart area renders empty. */}
|
|
441
522
|
<div className="p-card" style={{ padding: '1rem' }}>
|
|
442
|
-
<div className="flex" style={{ gap: '1rem', alignItems: 'center', marginBottom: '0.5rem' }}>
|
|
523
|
+
<div className="flex" style={{ gap: '1rem', alignItems: 'center', marginBottom: '0.5rem', flexWrap: 'wrap' }}>
|
|
443
524
|
<Dropdown
|
|
444
525
|
value={selectedView}
|
|
445
|
-
options={
|
|
526
|
+
options={allViews.map(v => ({ label: v.view.title ?? v.name, value: v.name }))}
|
|
446
527
|
onChange={(e) => setSelectedView(e.value)}
|
|
447
|
-
placeholder={
|
|
448
|
-
disabled={
|
|
528
|
+
placeholder={allViews.length === 0 ? 'No view defined' : 'Select a view'}
|
|
529
|
+
disabled={allViews.length === 0}
|
|
449
530
|
/>
|
|
450
531
|
<h3 style={{ margin: 0 }}>{selectedViewDef?.title ?? ''}</h3>
|
|
532
|
+
{/* Cycle picker — only visible for raw_trace views
|
|
533
|
+
AND when more than one cycle exists. The hook
|
|
534
|
+
handles the cycle-list discovery and "default
|
|
535
|
+
to latest" behaviour so this stays declarative. */}
|
|
536
|
+
{isRawTraceView && traceFetch.cycles.length > 1 && (
|
|
537
|
+
<>
|
|
538
|
+
<label htmlFor="chart-cycle-picker"
|
|
539
|
+
style={{ color: 'var(--text-secondary-color)' }}>Cycle:</label>
|
|
540
|
+
<Dropdown
|
|
541
|
+
inputId="chart-cycle-picker"
|
|
542
|
+
value={traceFetch.selectedCycle}
|
|
543
|
+
options={traceFetch.cycles.map(c => ({ label: `Cycle ${c}`, value: c }))}
|
|
544
|
+
onChange={(e) => traceFetch.setSelectedCycle(Number(e.value))}
|
|
545
|
+
style={{ minWidth: '8rem' }}
|
|
546
|
+
/>
|
|
547
|
+
<span style={{ color: 'var(--text-secondary-color)' }}>
|
|
548
|
+
of {traceFetch.cycles.length}
|
|
549
|
+
</span>
|
|
550
|
+
</>
|
|
551
|
+
)}
|
|
552
|
+
{/* Spacer pushes the reset-zoom button to the right
|
|
553
|
+
edge of the row, opposite the dropdown. The
|
|
554
|
+
pi-th-large icon mirrors a "view all" / "fit"
|
|
555
|
+
affordance — the chart.js zoom plugin calls
|
|
556
|
+
this resetZoom and we wire it on the chartRef
|
|
557
|
+
below. Customers wanted the affordance because
|
|
558
|
+
the chart's wheel/pinch zoom is easy to lose
|
|
559
|
+
track of mid-test. */}
|
|
560
|
+
<div style={{ flex: 1 }} />
|
|
561
|
+
<Button
|
|
562
|
+
icon="pi pi-th-large"
|
|
563
|
+
outlined
|
|
564
|
+
rounded
|
|
565
|
+
size="small"
|
|
566
|
+
onClick={() => chartRef.current?.resetZoom?.()}
|
|
567
|
+
disabled={!chartData}
|
|
568
|
+
tooltip="Reset chart zoom"
|
|
569
|
+
tooltipOptions={{ position: 'left' }}
|
|
570
|
+
aria-label="Reset chart zoom"
|
|
571
|
+
/>
|
|
451
572
|
</div>
|
|
452
|
-
<div style={{ height:
|
|
453
|
-
{
|
|
573
|
+
<div style={{ height: chartHeight, position: 'relative' }}>
|
|
574
|
+
{isRawTraceView && traceFetch.loading &&
|
|
575
|
+
<ChartOverlay>Loading raw data…</ChartOverlay>}
|
|
576
|
+
{isRawTraceView && traceFetch.error &&
|
|
577
|
+
<ChartOverlay>{traceFetch.error}</ChartOverlay>}
|
|
578
|
+
{chartData && <Line ref={chartRef} data={chartData} options={chartOptions} />}
|
|
454
579
|
</div>
|
|
455
580
|
</div>
|
|
456
581
|
|
|
457
582
|
<div className="p-card" style={{ padding: '1rem' }}>
|
|
458
583
|
<h3 style={{ marginTop: 0 }}>Cycle Data ({cycles.length})</h3>
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
584
|
+
{/* Size-to-content for small runs (≤ CYCLE_VIRTUAL_THRESHOLD
|
|
585
|
+
rows) so the Results panel below isn't pushed off-screen
|
|
586
|
+
on tests with one or two cycles. Larger runs flip to
|
|
587
|
+
virtual scrolling against `cycleTableHeight` so a
|
|
588
|
+
1000-cycle test doesn't render 1000 row elements at
|
|
589
|
+
once. Operators on test methods that always run a
|
|
590
|
+
fixed-size short cycle list see no scrollbar at all. */}
|
|
591
|
+
{(() => {
|
|
592
|
+
const useVirtual = cycles.length > CYCLE_VIRTUAL_THRESHOLD;
|
|
593
|
+
return (
|
|
594
|
+
<DataTable
|
|
595
|
+
value={cycles}
|
|
596
|
+
scrollable={useVirtual}
|
|
597
|
+
scrollHeight={useVirtual ? cycleTableHeight : undefined}
|
|
598
|
+
virtualScrollerOptions={useVirtual ? { itemSize: 38 } : undefined}
|
|
599
|
+
emptyMessage="No cycles yet."
|
|
600
|
+
>
|
|
601
|
+
{schema.cycle_fields.map(f => (
|
|
602
|
+
<Column key={f.name} field={f.name}
|
|
603
|
+
header={f.units ? `${f.name} (${f.units})` : f.name}
|
|
604
|
+
body={(row) => formatCell(row[f.name], f.type, f.scale)} />
|
|
605
|
+
))}
|
|
606
|
+
</DataTable>
|
|
607
|
+
);
|
|
608
|
+
})()}
|
|
472
609
|
</div>
|
|
473
610
|
|
|
474
611
|
<div className="p-card" style={{ padding: '1rem' }}>
|
|
@@ -851,26 +988,61 @@ const ResultsGrid: React.FC<{ schema: TestFieldDef[]; values: any }> = ({ schema
|
|
|
851
988
|
<div style={{ fontSize: '0.8em', color: 'var(--text-secondary-color)' }}>
|
|
852
989
|
{f.name}{f.units ? ` (${f.units})` : ''}
|
|
853
990
|
</div>
|
|
854
|
-
<div>{formatCell(values[f.name], f.type)}</div>
|
|
991
|
+
<div>{formatCell(values[f.name], f.type, f.scale)}</div>
|
|
855
992
|
</div>
|
|
856
993
|
))}
|
|
857
994
|
</div>
|
|
858
995
|
);
|
|
859
996
|
};
|
|
860
997
|
|
|
998
|
+
/** Row count above which the Cycle Data table switches into virtual-
|
|
999
|
+
* scroll mode with a fixed `cycleTableHeight`. At or below the
|
|
1000
|
+
* threshold the table sizes to its content so the Results panel
|
|
1001
|
+
* below doesn't get pushed off-screen on short runs. 30 is enough to
|
|
1002
|
+
* comfortably show a typical hand-tuned test session without
|
|
1003
|
+
* scrolling but small enough that virtualization kicks in well
|
|
1004
|
+
* before performance becomes a concern. */
|
|
1005
|
+
const CYCLE_VIRTUAL_THRESHOLD = 30;
|
|
1006
|
+
|
|
861
1007
|
const CHART_COLORS = [
|
|
862
1008
|
'#4ea8de', '#f59e0b', '#22c55e', '#a855f7',
|
|
863
1009
|
'#ef4444', '#14b8a6', '#eab308', '#ec4899',
|
|
864
1010
|
];
|
|
865
1011
|
const palette = (i: number) => CHART_COLORS[i % CHART_COLORS.length];
|
|
866
1012
|
|
|
1013
|
+
// Loading / error wash drawn over the chart area while a raw_trace
|
|
1014
|
+
// fetch is in flight. Centered, pointer-events-none so the operator
|
|
1015
|
+
// can still interact with the dropdown above.
|
|
1016
|
+
const ChartOverlay: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
1017
|
+
<div style={{ position: 'absolute', inset: 0, display: 'flex',
|
|
1018
|
+
alignItems: 'center', justifyContent: 'center',
|
|
1019
|
+
color: 'var(--text-secondary-color)', pointerEvents: 'none' }}>
|
|
1020
|
+
{children}
|
|
1021
|
+
</div>
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
// Axis labels work for both scatter (s.field) and raw_trace (s.column)
|
|
1025
|
+
// series. Whichever the view declared, that's what's used as a label
|
|
1026
|
+
// fallback when the explicit `label` is absent.
|
|
1027
|
+
const seriesLabel = (s: ChartSeries) => s.label ?? s.field ?? s.column ?? '';
|
|
867
1028
|
const leftAxisLabel = (v?: ChartView) =>
|
|
868
|
-
v?.y.filter(s => s.y_axis !== 'right').map(
|
|
1029
|
+
v?.y.filter(s => s.y_axis !== 'right').map(seriesLabel).join(' / ') ?? '';
|
|
869
1030
|
const rightAxisLabel = (v?: ChartView) =>
|
|
870
|
-
v?.y.filter(s => s.y_axis === 'right').map(
|
|
1031
|
+
v?.y.filter(s => s.y_axis === 'right').map(seriesLabel).join(' / ') ?? '';
|
|
871
1032
|
|
|
872
|
-
|
|
1033
|
+
/**
|
|
1034
|
+
* Format one value for a cycle / results / config cell.
|
|
1035
|
+
*
|
|
1036
|
+
* `scale`, when present and != 1, is applied to numeric values so the
|
|
1037
|
+
* cell displays in operator units while storage stays raw. Mirrors
|
|
1038
|
+
* the convention used by AutoCoreTagContext: `display = raw * scale`.
|
|
1039
|
+
* Non-numeric values pass through unchanged.
|
|
1040
|
+
*/
|
|
1041
|
+
const formatCell = (v: any, type: string, scale?: number): string => {
|
|
873
1042
|
if (v === null || v === undefined) return '';
|
|
1043
|
+
if (scale && scale !== 1 && typeof v === 'number' && Number.isFinite(v)) {
|
|
1044
|
+
v = v * scale;
|
|
1045
|
+
}
|
|
874
1046
|
if (type === 'f32' || type === 'f64') {
|
|
875
1047
|
return typeof v === 'number' ? v.toFixed(4) : String(v);
|
|
876
1048
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* built-in dialog inside <TestDataView>.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import 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
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|