@adcops/autocore-react 3.3.72 → 3.3.75

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.
@@ -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
- // Lazy fetch only runs on mount / when identifiers change.
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
- setRaw(resp.data ?? {});
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;
@@ -320,3 +320,12 @@
320
320
  }
321
321
  }
322
322
  }
323
+
324
+ /* Padding for OverlayPanels and Dialogs */
325
+ .p-overlaypanel .p-overlaypanel-content {
326
+ padding: 4mm !important;
327
+ }
328
+
329
+ .p-dialog .p-dialog-content {
330
+ padding: 4mm !important;
331
+ }