@adcops/autocore-react 3.3.79 → 3.3.83
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/AmsProvider.d.ts.map +1 -1
- package/dist/components/ams/AmsProvider.js +1 -1
- package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
- package/dist/components/ams/AssetDetailView.js +1 -1
- package/dist/components/ams/CalibrationEntryDialog.d.ts +15 -0
- package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -1
- package/dist/components/ams/CalibrationEntryDialog.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +3 -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.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +24 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.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/AmsProvider.tsx +4 -3
- package/src/components/ams/AssetDetailView.tsx +24 -2
- package/src/components/ams/CalibrationEntryDialog.tsx +75 -13
- package/src/components/tis/TestDataView.tsx +176 -68
- package/src/components/tis/TestRawDataView.tsx +15 -97
- package/src/components/tis/TestSetupForm.tsx +25 -9
- package/src/components/tis/TisProvider.tsx +33 -2
- package/src/components/tis/useRawCycleData.ts +157 -0
|
@@ -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
|
|
@@ -368,19 +368,35 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
368
368
|
};
|
|
369
369
|
|
|
370
370
|
/**
|
|
371
|
-
* If
|
|
372
|
-
*
|
|
373
|
-
* should pick from. The form renders that
|
|
374
|
-
* AMS assets instead of a free-form text
|
|
371
|
+
* If any `asset_ref` declares `select=by_id_field` with
|
|
372
|
+
* `from: "config.<field_name>"` for this field, return the
|
|
373
|
+
* asset_type the field should pick from. The form renders that
|
|
374
|
+
* field as a dropdown of AMS assets instead of a free-form text
|
|
375
|
+
* input.
|
|
375
376
|
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
377
|
+
* Refs are sourced from two places, matching what the server's
|
|
378
|
+
* resolver does at stage time:
|
|
379
|
+
* 1. Method-level (`schema.asset_refs`) — method-specific
|
|
380
|
+
* accessories.
|
|
381
|
+
* 2. Project-level (`tis.projectAssetRefs`) — system hardware
|
|
382
|
+
* that applies to every test method (e.g. the test surface
|
|
383
|
+
* picked per-test by id).
|
|
384
|
+
* Method-level wins on `field`-name collision, so a method can
|
|
385
|
+
* override the project default.
|
|
378
386
|
*/
|
|
379
387
|
const assetTypeForField = (field: TestFieldDef): string | null => {
|
|
380
|
-
const
|
|
388
|
+
const methodRefs = (schema?.asset_refs ?? []) as TisAssetRef[];
|
|
389
|
+
const projectRefs = (tis.projectAssetRefs ?? []) as TisAssetRef[];
|
|
381
390
|
const expectedFrom = `config.${field.name}`;
|
|
382
|
-
|
|
383
|
-
|
|
391
|
+
// Method wins on collision — iterate method refs first.
|
|
392
|
+
const methodHit = methodRefs.find(
|
|
393
|
+
r => r.select === 'by_id_field' && r.from === expectedFrom,
|
|
394
|
+
);
|
|
395
|
+
if (methodHit) return methodHit.asset_type;
|
|
396
|
+
const projectHit = projectRefs.find(
|
|
397
|
+
r => r.select === 'by_id_field' && r.from === expectedFrom,
|
|
398
|
+
);
|
|
399
|
+
return projectHit ? projectHit.asset_type : null;
|
|
384
400
|
};
|
|
385
401
|
|
|
386
402
|
const renderConfigField = (field: TestFieldDef) => {
|
|
@@ -91,8 +91,33 @@ export interface TisRunCacheEntry {
|
|
|
91
91
|
rawData: { [blobName: string]: any };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* One project-level asset_ref entry, in the wire shape returned by
|
|
96
|
+
* `tis.list_schemas`. Mirrors `Project.asset_refs` on the server.
|
|
97
|
+
* Carries only the fields the form's by_id_field lookup needs;
|
|
98
|
+
* other fields (label, description, calibration_required) are
|
|
99
|
+
* ignored but pass through untouched.
|
|
100
|
+
*/
|
|
101
|
+
export interface TisProjectAssetRef {
|
|
102
|
+
field: string;
|
|
103
|
+
asset_type: string;
|
|
104
|
+
select: 'by_location' | 'by_id_field';
|
|
105
|
+
/** Set when `select=by_id_field` — the dotted config path
|
|
106
|
+
* (`config.<field_name>`) whose value picks the asset. */
|
|
107
|
+
from?: string;
|
|
108
|
+
/** Set when `select=by_location` — the role/location key. */
|
|
109
|
+
location?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
export interface TisContextValue {
|
|
95
113
|
schemas: SchemaRegistry;
|
|
114
|
+
/** Project-level asset_refs from `tis.list_schemas`. Empty array
|
|
115
|
+
* when the project declares none. The HMI form merges these with
|
|
116
|
+
* the active method's own `asset_refs` before deciding whether to
|
|
117
|
+
* render a config field as a dropdown (by_id_field) or a free-form
|
|
118
|
+
* input. Method-level refs win on `field`-name collision, mirroring
|
|
119
|
+
* the server resolver. */
|
|
120
|
+
projectAssetRefs: TisProjectAssetRef[];
|
|
96
121
|
defaultMethodId: string;
|
|
97
122
|
schemasLoaded: boolean;
|
|
98
123
|
|
|
@@ -182,6 +207,7 @@ const EMPTY_SELECTION: TisSelection = {
|
|
|
182
207
|
|
|
183
208
|
const TisContext = createContext<TisContextValue>({
|
|
184
209
|
schemas: {},
|
|
210
|
+
projectAssetRefs: [],
|
|
185
211
|
defaultMethodId: '',
|
|
186
212
|
schemasLoaded: false,
|
|
187
213
|
state: EMPTY_STATE,
|
|
@@ -252,6 +278,7 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
252
278
|
const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
|
|
253
279
|
|
|
254
280
|
const [schemas, setSchemas] = useState<SchemaRegistry>({});
|
|
281
|
+
const [projectAssetRefs, setProjectAssetRefs] = useState<TisProjectAssetRef[]>([]);
|
|
255
282
|
const [defaultMethodId, setDefaultMethodId] = useState<string>(initialDefault ?? '');
|
|
256
283
|
const [schemasLoaded, setSchemasLoaded] = useState(false);
|
|
257
284
|
|
|
@@ -284,7 +311,11 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
284
311
|
if (resp?.success && resp.data) {
|
|
285
312
|
const methods = (resp.data.test_methods ?? {}) as SchemaRegistry;
|
|
286
313
|
const dflt = (resp.data.default_method_id ?? '') as string;
|
|
314
|
+
const refs = Array.isArray(resp.data.asset_refs)
|
|
315
|
+
? (resp.data.asset_refs as TisProjectAssetRef[])
|
|
316
|
+
: [];
|
|
287
317
|
setSchemas(methods);
|
|
318
|
+
setProjectAssetRefs(refs);
|
|
288
319
|
if (!initialDefault && dflt) setDefaultMethodId(dflt);
|
|
289
320
|
setSchemasLoaded(true);
|
|
290
321
|
} else {
|
|
@@ -620,14 +651,14 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
620
651
|
}, []);
|
|
621
652
|
|
|
622
653
|
const value: TisContextValue = useMemo(() => ({
|
|
623
|
-
schemas, defaultMethodId, schemasLoaded,
|
|
654
|
+
schemas, projectAssetRefs, defaultMethodId, schemasLoaded,
|
|
624
655
|
state, selection, setSelection,
|
|
625
656
|
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
626
657
|
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
627
658
|
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
628
659
|
fetchRuns, fetchRun, runCache,
|
|
629
660
|
}), [
|
|
630
|
-
schemas, defaultMethodId, schemasLoaded,
|
|
661
|
+
schemas, projectAssetRefs, defaultMethodId, schemasLoaded,
|
|
631
662
|
state, selection, setSelection,
|
|
632
663
|
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
633
664
|
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* useRawCycleData — shared hook that fetches `tis.list_raw` + `tis.read_raw`
|
|
5
|
+
* for one (project, method, run, blob_name) slice and tracks the
|
|
6
|
+
* operator's per-cycle selection. Used by:
|
|
7
|
+
*
|
|
8
|
+
* - <TestRawDataView> — the focused trace-only viewer.
|
|
9
|
+
* - <TestDataView> — the unified chart panel, which lazily fetches
|
|
10
|
+
* raw data only when a raw_trace view is selected
|
|
11
|
+
* (set `enabled` to false to short-circuit while
|
|
12
|
+
* a cycle_scatter view is active).
|
|
13
|
+
*
|
|
14
|
+
* Keeping the fetch logic in one place means the two components can't
|
|
15
|
+
* drift in how they discover cycles, default to "latest cycle", or
|
|
16
|
+
* unwrap the per-cycle envelope.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useContext, useEffect, useState } from 'react';
|
|
20
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
21
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
22
|
+
|
|
23
|
+
export interface UseRawCycleDataOptions {
|
|
24
|
+
projectId?: string;
|
|
25
|
+
methodId?: string;
|
|
26
|
+
runId?: string;
|
|
27
|
+
/** Blob name (e.g. "trace"); usually schema.raw_data.blob_name. */
|
|
28
|
+
blobName: string;
|
|
29
|
+
/** When false, the hook does no fetching and returns empty state.
|
|
30
|
+
* Wire to "is the current chart view a raw_trace?" so scatter
|
|
31
|
+
* selections don't trigger a blob round-trip. */
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UseRawCycleDataResult {
|
|
36
|
+
/** Per-cycle indices discovered on disk for this run/blob, sorted
|
|
37
|
+
* ascending. Empty when no raw_data has been written yet. */
|
|
38
|
+
cycles: number[];
|
|
39
|
+
selectedCycle: number | null;
|
|
40
|
+
setSelectedCycle: (n: number | null) => void;
|
|
41
|
+
/** Columnar payload unwrapped from the per-cycle envelope, or the
|
|
42
|
+
* blob itself for legacy flat shapes. `null` until the fetch lands. */
|
|
43
|
+
raw: Record<string, number[]> | null;
|
|
44
|
+
/** Full envelope as returned by `tis.read_raw`. Carries
|
|
45
|
+
* `cycle_index`, `cycle_fields`, and `context` alongside `data`. */
|
|
46
|
+
envelope: any | null;
|
|
47
|
+
loading: boolean;
|
|
48
|
+
error: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useRawCycleData(opts: UseRawCycleDataOptions): UseRawCycleDataResult {
|
|
52
|
+
const { projectId, methodId, runId, blobName, enabled } = opts;
|
|
53
|
+
const { invoke } = useContext(EventEmitterContext);
|
|
54
|
+
|
|
55
|
+
const [cycles, setCycles] = useState<number[]>([]);
|
|
56
|
+
const [selectedCycle, setSelectedCycle] = useState<number | null>(null);
|
|
57
|
+
const [raw, setRaw] = useState<Record<string, number[]> | null>(null);
|
|
58
|
+
const [envelope, setEnvelope] = useState<any | null>(null);
|
|
59
|
+
const [loading, setLoading] = useState(false);
|
|
60
|
+
const [error, setError] = useState<string | null>(null);
|
|
61
|
+
|
|
62
|
+
// Reset cycle state when the run identity (or blob) changes — the
|
|
63
|
+
// new slice has its own cycle list and its own "latest" target.
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
setCycles([]);
|
|
66
|
+
setSelectedCycle(null);
|
|
67
|
+
setRaw(null);
|
|
68
|
+
setEnvelope(null);
|
|
69
|
+
setError(null);
|
|
70
|
+
}, [projectId, methodId, runId, blobName]);
|
|
71
|
+
|
|
72
|
+
// Cycle-list discovery. Runs ahead of the data fetch so the cycle
|
|
73
|
+
// picker can render immediately. Filtered to the requested blob name.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!enabled || !projectId || !methodId || !runId) return;
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const resp: any = await invoke(
|
|
80
|
+
'tis.list_raw' as any, MessageType.Request as any,
|
|
81
|
+
{ project_id: projectId, method_id: methodId, run_id: runId } as any,
|
|
82
|
+
);
|
|
83
|
+
if (cancelled || !resp?.success) return;
|
|
84
|
+
const list: any[] = resp.data?.cycles ?? [];
|
|
85
|
+
const indices = list
|
|
86
|
+
.filter(c => c?.name === blobName && typeof c?.cycle_index === 'number')
|
|
87
|
+
.map(c => c.cycle_index as number)
|
|
88
|
+
.sort((a, b) => a - b);
|
|
89
|
+
setCycles(indices);
|
|
90
|
+
if (indices.length > 0) {
|
|
91
|
+
setSelectedCycle(prev => prev ?? indices[indices.length - 1]);
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Listing failure is non-fatal — the data fetch below
|
|
95
|
+
// still tries "latest" and the picker just stays empty.
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
return () => { cancelled = true; };
|
|
99
|
+
}, [enabled, projectId, methodId, runId, blobName, invoke]);
|
|
100
|
+
|
|
101
|
+
// Lazy blob fetch — runs whenever identifiers / selectedCycle change.
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!enabled || !projectId || !methodId || !runId) {
|
|
104
|
+
setRaw(null); setEnvelope(null); setLoading(false); setError(null);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let cancelled = false;
|
|
108
|
+
setLoading(true);
|
|
109
|
+
setError(null);
|
|
110
|
+
(async () => {
|
|
111
|
+
try {
|
|
112
|
+
const args: Record<string, any> = {
|
|
113
|
+
project_id: projectId, method_id: methodId,
|
|
114
|
+
run_id: runId, name: blobName,
|
|
115
|
+
};
|
|
116
|
+
if (selectedCycle != null) args.cycle_index = selectedCycle;
|
|
117
|
+
const resp: any = await invoke(
|
|
118
|
+
'tis.read_raw' as any, MessageType.Request as any, args as any,
|
|
119
|
+
);
|
|
120
|
+
if (cancelled) return;
|
|
121
|
+
if (resp?.success) {
|
|
122
|
+
const payload = resp.data ?? {};
|
|
123
|
+
setEnvelope(payload);
|
|
124
|
+
setRaw(unwrapEnvelope(payload));
|
|
125
|
+
} else {
|
|
126
|
+
setError(resp?.error_message ?? 'Failed to read raw data');
|
|
127
|
+
}
|
|
128
|
+
} catch (e: any) {
|
|
129
|
+
if (!cancelled) setError(String(e?.message ?? e));
|
|
130
|
+
} finally {
|
|
131
|
+
if (!cancelled) setLoading(false);
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
return () => { cancelled = true; };
|
|
135
|
+
}, [enabled, projectId, methodId, runId, blobName, selectedCycle, invoke]);
|
|
136
|
+
|
|
137
|
+
return { cycles, selectedCycle, setSelectedCycle, raw, envelope, loading, error };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `tis.read_raw` returns one of:
|
|
142
|
+
* - per-cycle envelope: `{ cycle_index, cycle_fields, context, data: { col: number[] } }`
|
|
143
|
+
* - legacy flat blob: `{ col: number[] }`
|
|
144
|
+
*
|
|
145
|
+
* Chart and CSV code only care about the columnar payload — peel off
|
|
146
|
+
* the envelope when present, otherwise pass the blob through. Returned
|
|
147
|
+
* shape is always `Record<string, number[]>` so consumers can index
|
|
148
|
+
* uniformly.
|
|
149
|
+
*/
|
|
150
|
+
export function unwrapEnvelope(blob: any): Record<string, number[]> {
|
|
151
|
+
if (!blob || typeof blob !== 'object') return {};
|
|
152
|
+
if ('data' in blob && blob.data && typeof blob.data === 'object'
|
|
153
|
+
&& Object.values(blob.data).some(v => Array.isArray(v))) {
|
|
154
|
+
return blob.data;
|
|
155
|
+
}
|
|
156
|
+
return blob;
|
|
157
|
+
}
|