@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.
@@ -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
- projectId: string;
10
- definitionId: string;
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 results `raw_data` blob (`{ colA: [...], colB: [...], ... }`) into
15
- * a CSV string. Keys with scalar (non-array) values are skipped; array lengths
16
- * are truncated to the shortest column so the grid is rectangular.
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
- export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = ({ projectId, definitionId }) => {
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 [downloadingRunId, setDownloadingRunId] = useState<string | null>(null);
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
- const resp: any = await invoke('results.list_tests' as any, MessageType.Request, {
74
- project_id: projectId,
75
- definition_id: definitionId
76
- } as any);
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
- }, [projectId, definitionId]);
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
- if (!runId) return;
98
- setDownloadingRunId(runId);
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('results.read_raw' as any, MessageType.Request, {
101
- project_id: projectId,
102
- definition_id: definitionId,
103
- run_id: runId,
104
- name: 'trace',
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('results.read_raw returned no data for', runId, resp?.error_message);
109
- alert(`No raw trace available for ${runId}${resp?.error_message ? `: ${resp.error_message}` : ''}`);
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(`Raw trace for ${runId} is empty or has no array columns.`);
156
+ alert(`${label} for ${runId} is empty or has no array columns.`);
116
157
  return;
117
158
  }
118
- downloadCsv(`${projectId}_${definitionId}_${runId}.csv`, csv);
159
+ downloadCsv(`${projectId}_${rowMethodId}_${runId}_${suffix}.csv`, csv);
119
160
  } catch (err) {
120
- console.error('Failed to download raw trace', err);
161
+ console.error(`Failed to download ${label}`, err);
121
162
  alert(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
122
163
  } finally {
123
- setDownloadingRunId(null);
164
+ setDownloading(null);
124
165
  }
125
166
  };
126
167
 
127
- // Cell-level helpers kept outside JSX so they're stable references.
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 }}>Test History: {definitionId}</h3>
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
- <Column field="run_id" header="Run ID" sortable style={{ minWidth: '12rem' }} />
159
- <Column field="start_time" header="Date/Time" sortable
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="Raw Data"
175
- style={{ width: '8rem' }}
176
- body={(rowData) => (
177
- <Button
178
- icon={downloadingRunId === rowData.run_id ? 'pi pi-spin pi-spinner' : 'pi pi-download'}
179
- label="CSV"
180
- size="small"
181
- outlined
182
- disabled={downloadingRunId !== null}
183
- onClick={() => handleDownload(rowData)}
184
- tooltip="Download raw_data/trace.json as CSV"
185
- tooltipOptions={{ position: 'left' }}
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 Results System.
5
- * Renders metadata header + cycle-scatter chart + virtual-scroll cycle
6
- * table + results table, and subscribes to live `results.cycle_added` /
7
- * `results.results_updated` broadcasts so the display updates as the
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/codegen_results.rs — kept local so the component
36
- // works without a hard dependency on any specific generated results.ts)
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 TestDefinition {
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: string;
71
- definitionId: string;
72
- runId: string;
73
- schema: TestDefinition;
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
- projectId, definitionId, runId, schema,
84
- throttleMs = 100,
85
- cycleTableHeight = '400px',
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.views ?? {})) {
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
- 'results.read_test' as any, MessageType.Request as any,
138
- { project_id: projectId, definition_id: definitionId, run_id: runId } as any);
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
- 'results.read_cycles' as any, MessageType.Request as any,
145
- { project_id: projectId, definition_id: definitionId, run_id: runId,
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, definitionId, runId, invoke]);
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?.definition_id === definitionId
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('results.cycle_added', onCycle);
178
- const id2 = subscribe('results.results_updated', onResults);
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, definitionId, runId, throttleMs]);
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} definitionId={definitionId}
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
- definitionId={definitionId}
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; definitionId: string;
335
+ projectId: string; methodId: string;
317
336
  canViewRaw: boolean; onViewRaw: () => void;
318
- }> = ({ meta, config, runId, projectId, definitionId, canViewRaw, onViewRaw }) => (
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 }}>{definitionId} {runId}</h2>
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
- {config && Object.keys(config).length > 0 && (
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(config).map(([k, v]) => (
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 Results System. Lazy-fetches
5
- * the columnar `raw_data/<blob>.json` for a single test and renders it
6
- * using any `raw_trace`-type view declared in the test schema. Supports
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, TestDefinition } from './TestDataView';
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: string;
35
- definitionId: string;
36
- runId: string;
37
- schema: TestDefinition;
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?: string;
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
- projectId, definitionId, runId, schema,
46
- blobName,
47
- chartHeight = '60vh',
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.views ?? {})) {
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.raw_data?.blob_name ?? 'trace';
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
- 'results.read_raw' as any, MessageType.Request as any,
80
- { project_id: projectId, definition_id: definitionId,
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, definitionId, runId, effectiveBlobName, invoke]);
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 definition." />;
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." />;