@adcops/autocore-react 3.3.50 → 3.3.57
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/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/ResultHistoryTable.d.ts +14 -2
- 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 +9 -5
- 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 +9 -5
- 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 -4
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +93 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -0
- package/dist/components/tis/TisProvider.js +1 -0
- package/dist/hub/HubWebSocket.d.ts +13 -0
- package/dist/hub/HubWebSocket.d.ts.map +1 -1
- package/dist/hub/HubWebSocket.js +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +22 -1
- package/src/components/tis/ResultHistoryTable.tsx +133 -48
- package/src/components/tis/TestDataView.tsx +70 -36
- package/src/components/tis/TestRawDataView.tsx +39 -22
- package/src/components/tis/TestSetupForm.tsx +155 -96
- package/src/components/tis/TisProvider.tsx +405 -0
- package/src/hub/HubWebSocket.ts +66 -3
|
@@ -4,16 +4,29 @@ import { Column } from 'primereact/column';
|
|
|
4
4
|
import { Button } from 'primereact/button';
|
|
5
5
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
6
6
|
import { MessageType } from '../../hub/CommandMessage';
|
|
7
|
+
import { useTis } from './TisProvider';
|
|
7
8
|
|
|
8
9
|
export interface ResultHistoryTableProps {
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Override the project scope. When omitted, the table reads from
|
|
12
|
+
* `useTisSelection().projectId` and follows the active project.
|
|
13
|
+
*/
|
|
14
|
+
projectId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional method-id filter. When omitted, the table aggregates every
|
|
17
|
+
* run under the project regardless of which method it belongs to —
|
|
18
|
+
* that's the default the operator wants on the History tab so that
|
|
19
|
+
* switching the loaded method on the Setup tab doesn't hide
|
|
20
|
+
* already-recorded data. When supplied, the table is scoped to that
|
|
21
|
+
* single method.
|
|
22
|
+
*/
|
|
23
|
+
methodId?: string;
|
|
11
24
|
}
|
|
12
25
|
|
|
13
26
|
/**
|
|
14
|
-
* Convert a
|
|
15
|
-
*
|
|
16
|
-
*
|
|
27
|
+
* Convert a raw_data blob (`{ colA: [...], colB: [...], ... }`) into a CSV
|
|
28
|
+
* string. Keys with scalar (non-array) values are skipped; array lengths are
|
|
29
|
+
* truncated to the shortest column so the grid is rectangular.
|
|
17
30
|
*
|
|
18
31
|
* Column order: `t` first if present (it's the canonical x-axis in every
|
|
19
32
|
* current test schema), then the remaining keys in their JSON order. Each
|
|
@@ -61,19 +74,30 @@ const downloadCsv = (filename: string, csv: string) => {
|
|
|
61
74
|
URL.revokeObjectURL(url);
|
|
62
75
|
};
|
|
63
76
|
|
|
64
|
-
|
|
77
|
+
type DownloadKind = 'raw' | 'filtered';
|
|
78
|
+
type InFlight = { runId: string; kind: DownloadKind };
|
|
79
|
+
|
|
80
|
+
export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) => {
|
|
81
|
+
const tis = useTis();
|
|
82
|
+
const projectId = props.projectId ?? tis.selection.projectId;
|
|
83
|
+
const methodId = props.methodId; // explicit override only — never auto-bound
|
|
84
|
+
|
|
65
85
|
const [tests, setTests] = useState<any[]>([]);
|
|
66
86
|
const [loading, setLoading] = useState(false);
|
|
67
|
-
const [
|
|
87
|
+
const [downloading, setDownloading] = useState<InFlight | null>(null);
|
|
68
88
|
const { invoke } = useContext(EventEmitterContext);
|
|
69
89
|
|
|
70
90
|
const loadTests = async () => {
|
|
91
|
+
if (!projectId) { setTests([]); return; }
|
|
71
92
|
setLoading(true);
|
|
72
93
|
try {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
// When the parent omits methodId, ask the server for all runs
|
|
95
|
+
// under the project (it walks every method directory). When
|
|
96
|
+
// methodId is set, the server returns only that method's runs.
|
|
97
|
+
const payload: any = { project_id: projectId };
|
|
98
|
+
if (methodId) payload.method_id = methodId;
|
|
99
|
+
|
|
100
|
+
const resp: any = await invoke('tis.list_tests' as any, MessageType.Request, payload as any);
|
|
77
101
|
if (resp.success && resp.data && resp.data.tests) {
|
|
78
102
|
setTests(resp.data.tests);
|
|
79
103
|
}
|
|
@@ -85,47 +109,69 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = ({ projectI
|
|
|
85
109
|
|
|
86
110
|
useEffect(() => {
|
|
87
111
|
loadTests();
|
|
88
|
-
|
|
112
|
+
// The table also wants to refresh when the active test ends —
|
|
113
|
+
// a finish_test broadcast flips active to false; the easiest
|
|
114
|
+
// signal is to rerun on activeRunId changes (a fresh test
|
|
115
|
+
// produces a new run_id, which we want to surface immediately).
|
|
116
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
117
|
+
}, [projectId, methodId, tis.state.activeRunId]);
|
|
89
118
|
|
|
90
119
|
const formatDate = (dateStr: string) => {
|
|
91
120
|
if (!dateStr) return '';
|
|
92
121
|
return new Date(dateStr).toLocaleString();
|
|
93
122
|
};
|
|
94
123
|
|
|
95
|
-
const handleDownload = async (rowData: any) => {
|
|
124
|
+
const handleDownload = async (rowData: any, kind: DownloadKind) => {
|
|
96
125
|
const runId = rowData?.run_id;
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
const rowMethodId = rowData?.method_id ?? methodId;
|
|
127
|
+
if (!runId || !rowMethodId) return;
|
|
128
|
+
|
|
129
|
+
// raw_data/ and filtered_data/ are symmetric dirs on disk; the same
|
|
130
|
+
// blob name ("trace") exists in both. Only the IPC topic and filename
|
|
131
|
+
// suffix change.
|
|
132
|
+
const topic = kind === 'raw' ? 'tis.read_raw' : 'tis.read_filtered';
|
|
133
|
+
const label = kind === 'raw' ? 'raw trace' : 'filtered trace';
|
|
134
|
+
const suffix = kind === 'raw' ? 'raw' : 'filtered';
|
|
135
|
+
|
|
136
|
+
setDownloading({ runId, kind });
|
|
99
137
|
try {
|
|
100
|
-
const resp: any = await invoke(
|
|
101
|
-
project_id:
|
|
102
|
-
|
|
103
|
-
run_id:
|
|
104
|
-
name:
|
|
138
|
+
const resp: any = await invoke(topic as any, MessageType.Request, {
|
|
139
|
+
project_id: projectId,
|
|
140
|
+
method_id: rowMethodId,
|
|
141
|
+
run_id: runId,
|
|
142
|
+
name: 'trace',
|
|
105
143
|
} as any);
|
|
106
144
|
|
|
107
145
|
if (!resp?.success || !resp.data) {
|
|
108
|
-
console.warn(
|
|
109
|
-
alert(
|
|
146
|
+
console.warn(`${topic} returned no data for`, runId, resp?.error_message);
|
|
147
|
+
alert(
|
|
148
|
+
`No ${label} available for ${runId}` +
|
|
149
|
+
(resp?.error_message ? `: ${resp.error_message}` : '')
|
|
150
|
+
);
|
|
110
151
|
return;
|
|
111
152
|
}
|
|
112
153
|
|
|
113
154
|
const csv = rawBlobToCsv(resp.data);
|
|
114
155
|
if (!csv) {
|
|
115
|
-
alert(
|
|
156
|
+
alert(`${label} for ${runId} is empty or has no array columns.`);
|
|
116
157
|
return;
|
|
117
158
|
}
|
|
118
|
-
downloadCsv(`${projectId}_${
|
|
159
|
+
downloadCsv(`${projectId}_${rowMethodId}_${runId}_${suffix}.csv`, csv);
|
|
119
160
|
} catch (err) {
|
|
120
|
-
console.error(
|
|
161
|
+
console.error(`Failed to download ${label}`, err);
|
|
121
162
|
alert(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
122
163
|
} finally {
|
|
123
|
-
|
|
164
|
+
setDownloading(null);
|
|
124
165
|
}
|
|
125
166
|
};
|
|
126
167
|
|
|
127
|
-
//
|
|
168
|
+
// sample_id is now a top-level field on test.json. Older test.json
|
|
169
|
+
// files (written before the rename) carry it nested in `config`; fall
|
|
170
|
+
// back to the nested form so we can still display the column for runs
|
|
171
|
+
// recorded by an older server.
|
|
128
172
|
const sampleIdOf = (rowData: any) => {
|
|
173
|
+
const top = typeof rowData?.sample_id === 'string' ? rowData.sample_id : '';
|
|
174
|
+
if (top) return top;
|
|
129
175
|
const cfg = rowData?.config;
|
|
130
176
|
if (cfg && typeof cfg === 'object' && typeof cfg.sample_id === 'string') {
|
|
131
177
|
return cfg.sample_id;
|
|
@@ -140,7 +186,11 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = ({ projectI
|
|
|
140
186
|
// own scrollbars instead of blowing out layout.
|
|
141
187
|
<div style={{ width: '100%', maxWidth: '100%', overflow: 'hidden', boxSizing: 'border-box' }}>
|
|
142
188
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
|
143
|
-
<h3 style={{ margin: 0 }}>
|
|
189
|
+
<h3 style={{ margin: 0 }}>
|
|
190
|
+
{projectId
|
|
191
|
+
? `Test History: ${projectId}${methodId ? ` / ${methodId}` : ''}`
|
|
192
|
+
: 'Test History (no project selected)'}
|
|
193
|
+
</h3>
|
|
144
194
|
<Button icon="pi pi-refresh" label="Refresh" onClick={loadTests} disabled={loading} />
|
|
145
195
|
</div>
|
|
146
196
|
|
|
@@ -154,13 +204,25 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = ({ projectI
|
|
|
154
204
|
scrollHeight="flex"
|
|
155
205
|
tableStyle={{ minWidth: 0 }}
|
|
156
206
|
style={{ width: '100%' }}
|
|
207
|
+
selectionMode="single"
|
|
208
|
+
onSelectionChange={(e: any) => {
|
|
209
|
+
const row = e.value;
|
|
210
|
+
if (row?.run_id) {
|
|
211
|
+
// Pin the run + its (project, method) so the Data
|
|
212
|
+
// and Raw Data views jump to the selected row
|
|
213
|
+
// when the operator clicks across tabs. Pass the
|
|
214
|
+
// method too so a click in the cross-method
|
|
215
|
+
// history view still resolves correctly.
|
|
216
|
+
tis.setSelection({
|
|
217
|
+
projectId: row.project_id ?? null,
|
|
218
|
+
methodId: row.method_id ?? null,
|
|
219
|
+
runId: row.run_id,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}}
|
|
157
223
|
>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
body={(rowData) => formatDate(rowData.start_time)}
|
|
161
|
-
style={{ minWidth: '12rem' }}
|
|
162
|
-
/>
|
|
163
|
-
<Column field="definition_id" header="Test Name" sortable style={{ minWidth: '10rem' }} />
|
|
224
|
+
{/* Sample ID is the primary user-visible label — operators
|
|
225
|
+
look for "where's sample SAMPLE-0042" before any other axis. */}
|
|
164
226
|
<Column header="Sample ID" sortable
|
|
165
227
|
body={sampleIdOf}
|
|
166
228
|
sortFunction={(e) => {
|
|
@@ -170,21 +232,44 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = ({ projectI
|
|
|
170
232
|
}}
|
|
171
233
|
style={{ minWidth: '8rem' }}
|
|
172
234
|
/>
|
|
235
|
+
<Column field="start_time" header="Date/Time" sortable
|
|
236
|
+
body={(rowData) => formatDate(rowData.start_time)}
|
|
237
|
+
style={{ minWidth: '12rem' }}
|
|
238
|
+
/>
|
|
239
|
+
<Column field="method_id" header="Test Method" sortable style={{ minWidth: '10rem' }} />
|
|
240
|
+
<Column field="run_id" header="Run ID" sortable style={{ minWidth: '12rem' }} />
|
|
173
241
|
<Column
|
|
174
|
-
header="
|
|
175
|
-
style={{ width: '
|
|
176
|
-
body={(rowData) =>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
242
|
+
header="Download"
|
|
243
|
+
style={{ width: '14rem' }}
|
|
244
|
+
body={(rowData) => {
|
|
245
|
+
const isRawBusy = downloading?.runId === rowData.run_id && downloading?.kind === 'raw';
|
|
246
|
+
const isFilteredBusy = downloading?.runId === rowData.run_id && downloading?.kind === 'filtered';
|
|
247
|
+
const anyBusy = downloading !== null;
|
|
248
|
+
return (
|
|
249
|
+
<div style={{ display: 'flex', gap: '0.4rem' }}>
|
|
250
|
+
<Button
|
|
251
|
+
icon={isRawBusy ? 'pi pi-spin pi-spinner' : 'pi pi-download'}
|
|
252
|
+
label="Raw"
|
|
253
|
+
size="small"
|
|
254
|
+
outlined
|
|
255
|
+
disabled={anyBusy}
|
|
256
|
+
onClick={() => handleDownload(rowData, 'raw')}
|
|
257
|
+
tooltip="Download raw_data/trace.json as CSV"
|
|
258
|
+
tooltipOptions={{ position: 'left' }}
|
|
259
|
+
/>
|
|
260
|
+
<Button
|
|
261
|
+
icon={isFilteredBusy ? 'pi pi-spin pi-spinner' : 'pi pi-download'}
|
|
262
|
+
label="Filtered"
|
|
263
|
+
size="small"
|
|
264
|
+
outlined
|
|
265
|
+
disabled={anyBusy}
|
|
266
|
+
onClick={() => handleDownload(rowData, 'filtered')}
|
|
267
|
+
tooltip="Download filtered_data/trace.json as CSV"
|
|
268
|
+
tooltipOptions={{ position: 'left' }}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}}
|
|
188
273
|
/>
|
|
189
274
|
</DataTable>
|
|
190
275
|
</div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
3
|
*
|
|
4
|
-
* TestDataView — standardized test-detail view for the
|
|
5
|
-
* Renders metadata header + cycle-scatter chart + virtual-scroll
|
|
6
|
-
* table + results table, and subscribes to live `
|
|
7
|
-
* `
|
|
4
|
+
* TestDataView — standardized test-detail view for the Test Information
|
|
5
|
+
* System. Renders metadata header + cycle-scatter chart + virtual-scroll
|
|
6
|
+
* cycle table + results table, and subscribes to live `tis.cycle_added`
|
|
7
|
+
* and `tis.results_updated` broadcasts so the display updates as the
|
|
8
8
|
* control program appends cycles.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -25,6 +25,7 @@ import { Line } from 'react-chartjs-2';
|
|
|
25
25
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
26
26
|
import { MessageType } from '../../hub/CommandMessage';
|
|
27
27
|
import { TestRawDataView } from './TestRawDataView';
|
|
28
|
+
import { useTis } from './TisProvider';
|
|
28
29
|
|
|
29
30
|
ChartJS.register(
|
|
30
31
|
CategoryScale, LinearScale, PointElement, LineElement,
|
|
@@ -32,8 +33,8 @@ ChartJS.register(
|
|
|
32
33
|
);
|
|
33
34
|
|
|
34
35
|
// -------------------------------------------------------------------------
|
|
35
|
-
// Types (mirror codegen/
|
|
36
|
-
//
|
|
36
|
+
// Types (mirror codegen/codegen_tis.rs — kept local so the component works
|
|
37
|
+
// without a hard dependency on any specific generated tis.ts)
|
|
37
38
|
// -------------------------------------------------------------------------
|
|
38
39
|
|
|
39
40
|
export interface TestFieldDef {
|
|
@@ -57,7 +58,7 @@ export interface RawDataShape {
|
|
|
57
58
|
columns: string[];
|
|
58
59
|
units?: { [col: string]: string };
|
|
59
60
|
}
|
|
60
|
-
export interface
|
|
61
|
+
export interface TestMethod {
|
|
61
62
|
project_fields: TestFieldDef[];
|
|
62
63
|
config_fields: TestFieldDef[];
|
|
63
64
|
cycle_fields: TestFieldDef[];
|
|
@@ -67,10 +68,14 @@ export interface TestDefinition {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export interface TestDataViewProps {
|
|
70
|
-
projectId
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
/** Optional override; defaults to `useTisSelection().projectId`. */
|
|
72
|
+
projectId?: string;
|
|
73
|
+
/** Optional override; defaults to `useTisSelection().methodId`. */
|
|
74
|
+
methodId?: string;
|
|
75
|
+
/** Optional override; defaults to `useTisSelection().runId`. */
|
|
76
|
+
runId?: string;
|
|
77
|
+
/** Optional override; defaults to `useTisSchemas()[methodId]`. */
|
|
78
|
+
schema?: TestMethod;
|
|
74
79
|
/** Minimum ms between display updates when broadcasts arrive. Default 100. */
|
|
75
80
|
throttleMs?: number;
|
|
76
81
|
/** Fixed cycle-table scroll height. Default "400px". */
|
|
@@ -79,11 +84,13 @@ export interface TestDataViewProps {
|
|
|
79
84
|
|
|
80
85
|
// -------------------------------------------------------------------------
|
|
81
86
|
|
|
82
|
-
export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
88
|
+
const tis = useTis();
|
|
89
|
+
const projectId = props.projectId ?? tis.selection.projectId;
|
|
90
|
+
const methodId = props.methodId ?? tis.selection.methodId;
|
|
91
|
+
const runId = props.runId ?? tis.selection.runId;
|
|
92
|
+
const schema = props.schema ?? (methodId ? (tis.schemas[methodId] as TestMethod) : undefined);
|
|
93
|
+
const { throttleMs = 100, cycleTableHeight = '400px' } = props;
|
|
87
94
|
const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
|
|
88
95
|
|
|
89
96
|
const [meta, setMeta] = useState<any>(null);
|
|
@@ -94,8 +101,8 @@ export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
|
94
101
|
// Scatter-capable views only — raw_trace lives in <TestRawDataView>.
|
|
95
102
|
const scatterViews = useMemo(() => {
|
|
96
103
|
const out: { name: string; view: ChartView }[] = [];
|
|
97
|
-
for (const [name, v] of Object.entries(schema
|
|
98
|
-
if (v.type === 'cycle_scatter') out.push({ name, view: v });
|
|
104
|
+
for (const [name, v] of Object.entries(schema?.views ?? {})) {
|
|
105
|
+
if ((v as ChartView).type === 'cycle_scatter') out.push({ name, view: v as ChartView });
|
|
99
106
|
}
|
|
100
107
|
return out;
|
|
101
108
|
}, [schema]);
|
|
@@ -130,19 +137,23 @@ export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
|
130
137
|
// Initial load
|
|
131
138
|
// -----------------------------------------------------------------
|
|
132
139
|
useEffect(() => {
|
|
140
|
+
if (!projectId || !methodId || !runId) {
|
|
141
|
+
setMeta(null); setCycles([]); setResults({});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
133
144
|
let cancelled = false;
|
|
134
145
|
(async () => {
|
|
135
146
|
try {
|
|
136
147
|
const testResp: any = await invoke(
|
|
137
|
-
'
|
|
138
|
-
{ project_id: projectId,
|
|
148
|
+
'tis.read_test' as any, MessageType.Request as any,
|
|
149
|
+
{ project_id: projectId, method_id: methodId, run_id: runId } as any);
|
|
139
150
|
if (!cancelled && testResp?.success) {
|
|
140
151
|
setMeta(testResp.data);
|
|
141
152
|
setResults(testResp.data.results ?? {});
|
|
142
153
|
}
|
|
143
154
|
const cyResp: any = await invoke(
|
|
144
|
-
'
|
|
145
|
-
{ project_id: projectId,
|
|
155
|
+
'tis.read_cycles' as any, MessageType.Request as any,
|
|
156
|
+
{ project_id: projectId, method_id: methodId, run_id: runId,
|
|
146
157
|
offset: 0, limit: 200, order: 'desc' } as any);
|
|
147
158
|
if (!cancelled && cyResp?.success) {
|
|
148
159
|
setCycles(cyResp.data.cycles ?? []);
|
|
@@ -152,7 +163,7 @@ export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
|
152
163
|
}
|
|
153
164
|
})();
|
|
154
165
|
return () => { cancelled = true; };
|
|
155
|
-
}, [projectId,
|
|
166
|
+
}, [projectId, methodId, runId, invoke]);
|
|
156
167
|
|
|
157
168
|
// -----------------------------------------------------------------
|
|
158
169
|
// Live broadcasts
|
|
@@ -160,7 +171,7 @@ export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
|
160
171
|
useEffect(() => {
|
|
161
172
|
const matches = (payload: any) =>
|
|
162
173
|
payload?.project_id === projectId
|
|
163
|
-
&& payload?.
|
|
174
|
+
&& payload?.method_id === methodId
|
|
164
175
|
&& payload?.run_id === runId;
|
|
165
176
|
|
|
166
177
|
const onCycle = (payload: any) => {
|
|
@@ -174,15 +185,15 @@ export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
|
174
185
|
scheduleFlush();
|
|
175
186
|
};
|
|
176
187
|
|
|
177
|
-
const id1 = subscribe('
|
|
178
|
-
const id2 = subscribe('
|
|
188
|
+
const id1 = subscribe('tis.cycle_added', onCycle);
|
|
189
|
+
const id2 = subscribe('tis.results_updated', onResults);
|
|
179
190
|
return () => {
|
|
180
191
|
unsubscribe(id1);
|
|
181
192
|
unsubscribe(id2);
|
|
182
193
|
if (flushTimer.current) { clearTimeout(flushTimer.current); flushTimer.current = null; }
|
|
183
194
|
};
|
|
184
195
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
185
|
-
}, [projectId,
|
|
196
|
+
}, [projectId, methodId, runId, throttleMs]);
|
|
186
197
|
|
|
187
198
|
// -----------------------------------------------------------------
|
|
188
199
|
// Chart data
|
|
@@ -241,10 +252,18 @@ export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
|
241
252
|
// -----------------------------------------------------------------
|
|
242
253
|
// Render
|
|
243
254
|
// -----------------------------------------------------------------
|
|
255
|
+
if (!projectId || !methodId || !runId || !schema) {
|
|
256
|
+
return (
|
|
257
|
+
<div className="p-card" style={{ padding: '1rem', color: 'var(--text-secondary-color)' }}>
|
|
258
|
+
No test selected. Pick a row from the History tab or start a run.
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
244
263
|
return (
|
|
245
264
|
<div className="vblock" style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
246
265
|
<Header meta={meta} config={meta?.config} runId={runId}
|
|
247
|
-
projectId={projectId}
|
|
266
|
+
projectId={projectId} methodId={methodId}
|
|
248
267
|
canViewRaw={!!schema.raw_data}
|
|
249
268
|
onViewRaw={() => setRawOpen(true)} />
|
|
250
269
|
|
|
@@ -297,7 +316,7 @@ export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
|
297
316
|
>
|
|
298
317
|
<TestRawDataView
|
|
299
318
|
projectId={projectId}
|
|
300
|
-
|
|
319
|
+
methodId={methodId}
|
|
301
320
|
runId={runId}
|
|
302
321
|
schema={schema}
|
|
303
322
|
/>
|
|
@@ -313,15 +332,29 @@ export const TestDataView: React.FC<TestDataViewProps> = ({
|
|
|
313
332
|
|
|
314
333
|
const Header: React.FC<{
|
|
315
334
|
meta: any; config: any; runId: string;
|
|
316
|
-
projectId: string;
|
|
335
|
+
projectId: string; methodId: string;
|
|
317
336
|
canViewRaw: boolean; onViewRaw: () => void;
|
|
318
|
-
}> = ({ meta, config, runId, projectId,
|
|
337
|
+
}> = ({ meta, config, runId, projectId, methodId, canViewRaw, onViewRaw }) => {
|
|
338
|
+
// Sample ID is the field operators look for first; pull it from the
|
|
339
|
+
// top-level test.json field, falling back to the legacy nested location.
|
|
340
|
+
const sampleId =
|
|
341
|
+
(typeof meta?.sample_id === 'string' && meta.sample_id) ||
|
|
342
|
+
(typeof meta?.config?.sample_id === 'string' && meta.config.sample_id) ||
|
|
343
|
+
'';
|
|
344
|
+
|
|
345
|
+
// The body of the user-facing config grid should not surface the
|
|
346
|
+
// sample_id again — it's already in the header.
|
|
347
|
+
const configWithoutSampleId = config && typeof config === 'object'
|
|
348
|
+
? Object.fromEntries(Object.entries(config).filter(([k]) => k !== 'sample_id'))
|
|
349
|
+
: {};
|
|
350
|
+
|
|
351
|
+
return (
|
|
319
352
|
<div className="p-card" style={{ padding: '1rem' }}>
|
|
320
353
|
<div className="flex" style={{ justifyContent: 'space-between', alignItems: 'flex-start', gap: '1rem' }}>
|
|
321
354
|
<div>
|
|
322
|
-
<h2 style={{ margin: 0 }}>{
|
|
355
|
+
<h2 style={{ margin: 0 }}>{sampleId || runId}</h2>
|
|
323
356
|
<div style={{ color: 'var(--text-secondary-color)', fontSize: '0.85em' }}>
|
|
324
|
-
project: {projectId}
|
|
357
|
+
project: {projectId} · method: {methodId} · run: {runId}
|
|
325
358
|
{meta?.start_time && <> · started: {new Date(meta.start_time).toLocaleString()}</>}
|
|
326
359
|
</div>
|
|
327
360
|
</div>
|
|
@@ -329,17 +362,18 @@ const Header: React.FC<{
|
|
|
329
362
|
<Button icon="pi pi-chart-line" label="View Raw Data" onClick={onViewRaw} outlined />
|
|
330
363
|
)}
|
|
331
364
|
</div>
|
|
332
|
-
{
|
|
365
|
+
{Object.keys(configWithoutSampleId).length > 0 && (
|
|
333
366
|
<div style={{ marginTop: '0.75rem', display: 'grid',
|
|
334
367
|
gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))',
|
|
335
368
|
gap: '0.25rem 1rem', fontSize: '0.9em' }}>
|
|
336
|
-
{Object.entries(
|
|
369
|
+
{Object.entries(configWithoutSampleId).map(([k, v]) => (
|
|
337
370
|
<div key={k}><strong>{k}:</strong> {formatCell(v, 'string')}</div>
|
|
338
371
|
))}
|
|
339
372
|
</div>
|
|
340
373
|
)}
|
|
341
374
|
</div>
|
|
342
|
-
);
|
|
375
|
+
);
|
|
376
|
+
};
|
|
343
377
|
|
|
344
378
|
const ResultsGrid: React.FC<{ schema: TestFieldDef[]; values: any }> = ({ schema, values }) => {
|
|
345
379
|
if (!values || Object.keys(values).length === 0) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
3
|
*
|
|
4
|
-
* TestRawDataView — raw-trace viewer for the
|
|
5
|
-
* the columnar `raw_data/<blob>.json` for a single test and
|
|
6
|
-
* using any `raw_trace`-type view declared in the test schema.
|
|
7
|
-
* pinch-zoom, wheel-zoom, and drag-pan.
|
|
4
|
+
* TestRawDataView — raw-trace viewer for the Test Information System.
|
|
5
|
+
* Lazy-fetches the columnar `raw_data/<blob>.json` for a single test and
|
|
6
|
+
* renders it using any `raw_trace`-type view declared in the test schema.
|
|
7
|
+
* Supports pinch-zoom, wheel-zoom, and drag-pan.
|
|
8
8
|
*
|
|
9
9
|
* Can be used either standalone (e.g. a dedicated route) or from the
|
|
10
10
|
* built-in dialog inside <TestDataView>.
|
|
@@ -23,7 +23,8 @@ import { Line } from 'react-chartjs-2';
|
|
|
23
23
|
|
|
24
24
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
25
25
|
import { MessageType } from '../../hub/CommandMessage';
|
|
26
|
-
import type { ChartView,
|
|
26
|
+
import type { ChartView, TestMethod } from './TestDataView';
|
|
27
|
+
import { useTis } from './TisProvider';
|
|
27
28
|
|
|
28
29
|
ChartJS.register(
|
|
29
30
|
CategoryScale, LinearScale, PointElement, LineElement,
|
|
@@ -31,21 +32,27 @@ ChartJS.register(
|
|
|
31
32
|
);
|
|
32
33
|
|
|
33
34
|
export interface TestRawDataViewProps {
|
|
34
|
-
projectId
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
/** Optional override; defaults to `useTisSelection().projectId`. */
|
|
36
|
+
projectId?: string;
|
|
37
|
+
/** Optional override; defaults to `useTisSelection().methodId`. */
|
|
38
|
+
methodId?: string;
|
|
39
|
+
/** Optional override; defaults to `useTisSelection().runId`. */
|
|
40
|
+
runId?: string;
|
|
41
|
+
/** Optional override; defaults to `useTisSchemas()[methodId]`. */
|
|
42
|
+
schema?: TestMethod;
|
|
38
43
|
/** Override the blob name (default: schema.raw_data.blob_name). */
|
|
39
|
-
blobName?:
|
|
44
|
+
blobName?: string;
|
|
40
45
|
/** Fixed chart height. Default "60vh". */
|
|
41
46
|
chartHeight?: string;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
export const TestRawDataView: React.FC<TestRawDataViewProps> = ({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
export const TestRawDataView: React.FC<TestRawDataViewProps> = (props) => {
|
|
50
|
+
const tis = useTis();
|
|
51
|
+
const projectId = props.projectId ?? tis.selection.projectId;
|
|
52
|
+
const methodId = props.methodId ?? tis.selection.methodId;
|
|
53
|
+
const runId = props.runId ?? tis.selection.runId;
|
|
54
|
+
const schema = props.schema ?? (methodId ? (tis.schemas[methodId] as TestMethod) : undefined);
|
|
55
|
+
const { blobName, chartHeight = '60vh' } = props;
|
|
49
56
|
const { invoke } = useContext(EventEmitterContext);
|
|
50
57
|
|
|
51
58
|
const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
|
|
@@ -56,8 +63,8 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = ({
|
|
|
56
63
|
// raw_trace-capable views only — cycle scatter lives in <TestDataView>.
|
|
57
64
|
const traceViews = useMemo(() => {
|
|
58
65
|
const out: { name: string; view: ChartView }[] = [];
|
|
59
|
-
for (const [name, v] of Object.entries(schema
|
|
60
|
-
if (v.type === 'raw_trace') out.push({ name, view: v });
|
|
66
|
+
for (const [name, v] of Object.entries(schema?.views ?? {})) {
|
|
67
|
+
if ((v as ChartView).type === 'raw_trace') out.push({ name, view: v as ChartView });
|
|
61
68
|
}
|
|
62
69
|
return out;
|
|
63
70
|
}, [schema]);
|
|
@@ -66,18 +73,22 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = ({
|
|
|
66
73
|
traceViews.length > 0 ? traceViews[0].name : null,
|
|
67
74
|
);
|
|
68
75
|
|
|
69
|
-
const effectiveBlobName = blobName ?? schema
|
|
76
|
+
const effectiveBlobName = blobName ?? schema?.raw_data?.blob_name ?? 'trace';
|
|
70
77
|
|
|
71
78
|
// Lazy fetch — only runs on mount / when identifiers change.
|
|
72
79
|
useEffect(() => {
|
|
80
|
+
if (!projectId || !methodId || !runId) {
|
|
81
|
+
setRaw(null); setLoading(false); setError(null);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
73
84
|
let cancelled = false;
|
|
74
85
|
setLoading(true);
|
|
75
86
|
setError(null);
|
|
76
87
|
(async () => {
|
|
77
88
|
try {
|
|
78
89
|
const resp: any = await invoke(
|
|
79
|
-
'
|
|
80
|
-
{ project_id: projectId,
|
|
90
|
+
'tis.read_raw' as any, MessageType.Request as any,
|
|
91
|
+
{ project_id: projectId, method_id: methodId,
|
|
81
92
|
run_id: runId, name: effectiveBlobName } as any);
|
|
82
93
|
if (cancelled) return;
|
|
83
94
|
if (resp?.success) {
|
|
@@ -92,7 +103,7 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = ({
|
|
|
92
103
|
}
|
|
93
104
|
})();
|
|
94
105
|
return () => { cancelled = true; };
|
|
95
|
-
}, [projectId,
|
|
106
|
+
}, [projectId, methodId, runId, effectiveBlobName, invoke]);
|
|
96
107
|
|
|
97
108
|
const chartData = useMemo(() => {
|
|
98
109
|
if (!raw || !selectedView) return null;
|
|
@@ -148,8 +159,14 @@ export const TestRawDataView: React.FC<TestRawDataViewProps> = ({
|
|
|
148
159
|
},
|
|
149
160
|
}), [selectedViewDef, usesRightAxis]);
|
|
150
161
|
|
|
162
|
+
if (!projectId || !methodId || !runId) {
|
|
163
|
+
return <EmptyState message="No test selected." />;
|
|
164
|
+
}
|
|
165
|
+
if (!schema) {
|
|
166
|
+
return <EmptyState message="Schema not loaded yet." />;
|
|
167
|
+
}
|
|
151
168
|
if (!schema.raw_data) {
|
|
152
|
-
return <EmptyState message="No raw_data is declared for this test
|
|
169
|
+
return <EmptyState message="No raw_data is declared for this test method." />;
|
|
153
170
|
}
|
|
154
171
|
if (traceViews.length === 0) {
|
|
155
172
|
return <EmptyState message="No raw_trace views declared. Add one to schema.views in project.json." />;
|