@adcops/autocore-react 3.3.115 → 3.3.118
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 +10 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/CycleConfigDialog.d.ts +39 -0
- package/dist/components/tis/CycleConfigDialog.d.ts.map +1 -0
- package/dist/components/tis/CycleConfigDialog.js +1 -0
- package/dist/components/tis/CycleConfigForm.d.ts +22 -0
- package/dist/components/tis/CycleConfigForm.d.ts.map +1 -0
- package/dist/components/tis/CycleConfigForm.js +1 -0
- package/dist/components/tis/CycleStatusStrip.d.ts +10 -0
- package/dist/components/tis/CycleStatusStrip.d.ts.map +1 -0
- package/dist/components/tis/CycleStatusStrip.js +1 -0
- package/dist/components/tis/TestDataView.d.ts +4 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestFieldRow.d.ts +151 -0
- package/dist/components/tis/TestFieldRow.d.ts.map +1 -0
- package/dist/components/tis/TestFieldRow.js +1 -0
- package/dist/components/tis/TestSetupForm.d.ts +15 -51
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TestSetupView.d.ts.map +1 -1
- package/dist/components/tis/TestSetupView.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +80 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/TisConfigEditor.js +1 -1
- package/dist/components/tis-editor/editor/CycleConfigSection.d.ts +19 -0
- package/dist/components/tis-editor/editor/CycleConfigSection.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/CycleConfigSection.js +1 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
- package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +3 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
- package/dist/components/tis-editor/types.d.ts +28 -1
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.js +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +14 -0
- package/src/components/tis/CycleConfigDialog.tsx +173 -0
- package/src/components/tis/CycleConfigForm.tsx +192 -0
- package/src/components/tis/CycleStatusStrip.tsx +84 -0
- package/src/components/tis/TestDataView.tsx +19 -3
- package/src/components/tis/TestFieldRow.tsx +297 -0
- package/src/components/tis/TestSetupForm.tsx +52 -212
- package/src/components/tis/TestSetupView.tsx +60 -3
- package/src/components/tis/TisProvider.tsx +191 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +1 -0
- package/src/components/tis-editor/editor/CycleConfigSection.tsx +93 -0
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +10 -0
- package/src/components/tis-editor/editor/MethodFormEditor.tsx +38 -6
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +22 -2
- package/src/components/tis-editor/editor/ViewsEditor.tsx +7 -0
- package/src/components/tis-editor/types.ts +34 -1
- package/src/components/tis-editor/validation.ts +34 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the same tabbed layout and scroll behaviour without copy-pasting a
|
|
7
7
|
* per-project `TestInformationView`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Up to three tabs, all self-driving from <TisProvider> (no prop threading):
|
|
10
10
|
*
|
|
11
11
|
* Project tab — <ProjectSelector> stays pinned at the top; the
|
|
12
12
|
* <ResultHistoryTable> (Test History) scrolls beneath
|
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
* Test tab — <TestSetupForm>. Its "Test Setup" header (Sample ID,
|
|
15
15
|
* Test Method, Configuration) stays pinned; the "Test
|
|
16
16
|
* Configuration" field list scrolls when it overflows.
|
|
17
|
+
* Cycle tab — <CycleConfigForm>, the per-row annotation recorded onto
|
|
18
|
+
* each cycle. Present only when the selected method
|
|
19
|
+
* declares `cycle_config_fields`, and badged when a
|
|
20
|
+
* required entry is missing. Auto-selected when a run
|
|
21
|
+
* goes active: Test setup is a pre-run concern, the
|
|
22
|
+
* annotation is a during-run one.
|
|
23
|
+
*
|
|
24
|
+
* The Cycle tab is deliberately not the only place this state is visible —
|
|
25
|
+
* an operator about to press Start is looking at the footer, not at a tab.
|
|
26
|
+
* Put <CycleStatusStrip> beside the Start button for that.
|
|
17
27
|
*
|
|
18
28
|
* The whole view fills its parent's height and confines overflow to the
|
|
19
29
|
* inner scroll regions, so the tab strip and the section headers stay
|
|
@@ -22,11 +32,13 @@
|
|
|
22
32
|
* `min-height: 0`, or a fixed-height panel) to get that behaviour.
|
|
23
33
|
*/
|
|
24
34
|
|
|
25
|
-
import React from 'react';
|
|
35
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
26
36
|
import { TabView, TabPanel } from 'primereact/tabview';
|
|
27
37
|
import { ProjectSelector } from './ProjectSelector';
|
|
28
38
|
import { ResultHistoryTable } from './ResultHistoryTable';
|
|
29
39
|
import { TestSetupForm } from './TestSetupForm';
|
|
40
|
+
import { CycleConfigForm } from './CycleConfigForm';
|
|
41
|
+
import { useTis } from './TisProvider';
|
|
30
42
|
|
|
31
43
|
export interface TestSetupViewProps {
|
|
32
44
|
/** Extra class names applied to the root element. */
|
|
@@ -40,9 +52,25 @@ export const TestSetupView: React.FC<TestSetupViewProps> = ({ className, style }
|
|
|
40
52
|
? `ac-test-setup-view ${className}`
|
|
41
53
|
: 'ac-test-setup-view';
|
|
42
54
|
|
|
55
|
+
const tis = useTis();
|
|
56
|
+
const hasCycleConfig = tis.cycleConfigFields.length > 0;
|
|
57
|
+
const cycleIncomplete = hasCycleConfig && !tis.cycleConfig.ready;
|
|
58
|
+
|
|
59
|
+
// Which tab is showing. Controlled so the view can move the operator to
|
|
60
|
+
// the Cycle tab when a run goes active — the annotation is what they
|
|
61
|
+
// need next, and Test setup is already done by then.
|
|
62
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
63
|
+
const prevActive = useRef(false);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (tis.state.active && !prevActive.current && hasCycleConfig) {
|
|
66
|
+
setActiveIndex(2);
|
|
67
|
+
}
|
|
68
|
+
prevActive.current = tis.state.active;
|
|
69
|
+
}, [tis.state.active, hasCycleConfig]);
|
|
70
|
+
|
|
43
71
|
return (
|
|
44
72
|
<div className={rootClass} style={style}>
|
|
45
|
-
<TabView>
|
|
73
|
+
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
|
46
74
|
{/* Project tab: pick or create a project, then browse its
|
|
47
75
|
history. ResultHistoryTable is project-scoped (across
|
|
48
76
|
methods) by default, so this is the natural place to
|
|
@@ -66,6 +94,35 @@ export const TestSetupView: React.FC<TestSetupViewProps> = ({ className, style }
|
|
|
66
94
|
<TabPanel header="Test">
|
|
67
95
|
<TestSetupForm />
|
|
68
96
|
</TabPanel>
|
|
97
|
+
|
|
98
|
+
{/* Cycle tab: per-row operator annotation. Rendered only
|
|
99
|
+
when the method declares any — a method that records
|
|
100
|
+
nothing by hand shouldn't grow an empty tab. The header
|
|
101
|
+
carries a warning marker when a required entry is
|
|
102
|
+
missing, so the state is legible from the tab strip
|
|
103
|
+
without opening it. */}
|
|
104
|
+
{hasCycleConfig && (
|
|
105
|
+
<TabPanel
|
|
106
|
+
header={
|
|
107
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
108
|
+
Cycle
|
|
109
|
+
{cycleIncomplete && (
|
|
110
|
+
<i
|
|
111
|
+
className="pi pi-exclamation-circle"
|
|
112
|
+
style={{ color: 'var(--red-500)' }}
|
|
113
|
+
aria-label="Cycle Configuration incomplete"
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
</span>
|
|
117
|
+
}
|
|
118
|
+
>
|
|
119
|
+
<div className="ac-test-setup-view__tab">
|
|
120
|
+
<div className="ac-test-setup-view__scroll">
|
|
121
|
+
<CycleConfigForm />
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</TabPanel>
|
|
125
|
+
)}
|
|
69
126
|
</TabView>
|
|
70
127
|
</div>
|
|
71
128
|
);
|
|
@@ -29,6 +29,7 @@ import React, {
|
|
|
29
29
|
} from 'react';
|
|
30
30
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
31
31
|
import { MessageType } from '../../hub/CommandMessage';
|
|
32
|
+
import type { CycleConfigScope, TestFieldDef } from './TestFieldRow';
|
|
32
33
|
|
|
33
34
|
// -------------------------------------------------------------------------
|
|
34
35
|
// Types
|
|
@@ -68,6 +69,37 @@ export interface TisLiveState {
|
|
|
68
69
|
lastStartError: string;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Live Cycle Configuration state for the selected (project, method) —
|
|
74
|
+
* the operator's per-row annotation. Mirrors the server's
|
|
75
|
+
* `tis.cycle_config` payload one-for-one; the server owns every rule
|
|
76
|
+
* (which boundary latches, when autoclear fires, whether the gate is
|
|
77
|
+
* satisfied) so the HMI never has to re-derive them and can never
|
|
78
|
+
* disagree with the `tis.cycle_ready` scalar the machine gates on.
|
|
79
|
+
*/
|
|
80
|
+
export interface TisCycleConfigState {
|
|
81
|
+
projectId: string;
|
|
82
|
+
methodId: string;
|
|
83
|
+
/** `'run'` = one latch per Start; `'cycle'` = one per cycle. */
|
|
84
|
+
scope: CycleConfigScope;
|
|
85
|
+
/** Method asks for the entry dialog at each boundary. */
|
|
86
|
+
promptBeforeCycle: boolean;
|
|
87
|
+
/** What the operator has entered but no row has consumed yet (RAW). */
|
|
88
|
+
values: Record<string, any>;
|
|
89
|
+
/** The snapshot the cycle now running will be stamped with, or null
|
|
90
|
+
* when nothing is latched. Non-null means the fields are locked for
|
|
91
|
+
* the row in flight — editing them targets the *next* row. */
|
|
92
|
+
latched: Record<string, any> | null;
|
|
93
|
+
/** False while any `required` field is empty. The same condition
|
|
94
|
+
* drives `tis.cycle_ready`, which gates the machine. */
|
|
95
|
+
ready: boolean;
|
|
96
|
+
/** Names of the required-but-empty fields, in declared order. */
|
|
97
|
+
missing: string[];
|
|
98
|
+
/** A `prompt_before_cycle` method has reached a boundary with nothing
|
|
99
|
+
* latched — the edge on which <CycleConfigDialog> opens. */
|
|
100
|
+
promptDue: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
71
103
|
export interface TisSelection {
|
|
72
104
|
projectId: string;
|
|
73
105
|
methodId: string;
|
|
@@ -208,6 +240,31 @@ export interface TisContextValue {
|
|
|
208
240
|
* operator accepts a different one in the picker dialog. */
|
|
209
241
|
setConfigurationName: (name: string) => void;
|
|
210
242
|
|
|
243
|
+
// -----------------------------------------------------------------
|
|
244
|
+
// Cycle Configuration — the operator's per-row annotation. Lives on
|
|
245
|
+
// the provider (like `stagedConfig`) so the Cycle tab, the status
|
|
246
|
+
// strip beside Start, and the prompt dialog all read one state and
|
|
247
|
+
// the values survive a tab switch.
|
|
248
|
+
// -----------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/** Live state for the selected method. Server-owned; every mutation
|
|
251
|
+
* round-trips so the HMI and the `tis.cycle_ready` gate agree. */
|
|
252
|
+
cycleConfig: TisCycleConfigState;
|
|
253
|
+
/** The selected method's `cycle_config_fields`, in declared order.
|
|
254
|
+
* Empty when the method doesn't use the feature — which is the
|
|
255
|
+
* signal to hide the Cycle tab entirely. */
|
|
256
|
+
cycleConfigFields: TestFieldDef[];
|
|
257
|
+
/** Merge a sparse patch of RAW values into the operator's pending
|
|
258
|
+
* entry. An explicit null/'' clears a field. Resolves once the
|
|
259
|
+
* server has applied it and the state has been refreshed. */
|
|
260
|
+
setCycleConfigValues: (patch: Record<string, any>) => Promise<void>;
|
|
261
|
+
/** Wipe every pending value regardless of `autoclear`. Leaves the
|
|
262
|
+
* latch alone — a cycle already running keeps its label. */
|
|
263
|
+
clearCycleConfig: () => Promise<void>;
|
|
264
|
+
/** Re-read Cycle Configuration state from the server. Called on
|
|
265
|
+
* selection change; broadcasts keep it fresh after that. */
|
|
266
|
+
refreshCycleConfig: () => Promise<void>;
|
|
267
|
+
|
|
211
268
|
/** Fetch the run list for a (project, method?) pair. Method may be
|
|
212
269
|
* omitted to aggregate runs across every method in the project —
|
|
213
270
|
* the History tab uses this. */
|
|
@@ -232,6 +289,29 @@ const EMPTY_SELECTION: TisSelection = {
|
|
|
232
289
|
projectId: '', methodId: '', sampleId: '', runId: '',
|
|
233
290
|
};
|
|
234
291
|
|
|
292
|
+
/** No method selected, or a method that declares no annotation columns.
|
|
293
|
+
* `ready: true` is the safe default — a method without cycle_config_fields
|
|
294
|
+
* must never look gated. */
|
|
295
|
+
const EMPTY_CYCLE_CONFIG: TisCycleConfigState = {
|
|
296
|
+
projectId: '', methodId: '', scope: 'run', promptBeforeCycle: false,
|
|
297
|
+
values: {}, latched: null, ready: true, missing: [], promptDue: false,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/** Normalise a server `tis.cycle_config` payload into provider state. */
|
|
301
|
+
const cycleConfigFromPayload = (p: any): TisCycleConfigState => ({
|
|
302
|
+
projectId: String(p?.project_id ?? ''),
|
|
303
|
+
methodId: String(p?.method_id ?? ''),
|
|
304
|
+
scope: p?.scope === 'cycle' ? 'cycle' : 'run',
|
|
305
|
+
promptBeforeCycle: !!p?.prompt_before_cycle,
|
|
306
|
+
values: (p?.values && typeof p.values === 'object') ? p.values : {},
|
|
307
|
+
latched: (p?.latched && typeof p.latched === 'object') ? p.latched : null,
|
|
308
|
+
// Absent `ready` means a server that predates the field — treat as
|
|
309
|
+
// ungated rather than locking an operator out of a working machine.
|
|
310
|
+
ready: p?.ready !== false,
|
|
311
|
+
missing: Array.isArray(p?.missing) ? p.missing : [],
|
|
312
|
+
promptDue: !!p?.prompt_due,
|
|
313
|
+
});
|
|
314
|
+
|
|
235
315
|
const TisContext = createContext<TisContextValue>({
|
|
236
316
|
schemas: {},
|
|
237
317
|
projectAssetRefs: [],
|
|
@@ -255,6 +335,11 @@ const TisContext = createContext<TisContextValue>({
|
|
|
255
335
|
markDefaultsAppliedForMethod: () => {},
|
|
256
336
|
configurationName: '',
|
|
257
337
|
setConfigurationName: () => {},
|
|
338
|
+
cycleConfig: EMPTY_CYCLE_CONFIG,
|
|
339
|
+
cycleConfigFields: [],
|
|
340
|
+
setCycleConfigValues: async () => {},
|
|
341
|
+
clearCycleConfig: async () => {},
|
|
342
|
+
refreshCycleConfig: async () => {},
|
|
258
343
|
fetchRuns: async () => [],
|
|
259
344
|
fetchRun: async () => null,
|
|
260
345
|
runCache: {},
|
|
@@ -699,6 +784,90 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
699
784
|
setConfigurationName('');
|
|
700
785
|
}, []);
|
|
701
786
|
|
|
787
|
+
// -----------------------------------------------------------------
|
|
788
|
+
// Cycle Configuration
|
|
789
|
+
//
|
|
790
|
+
// The server owns the rules — which boundary latches, when autoclear
|
|
791
|
+
// fires, whether the gate is satisfied — so this is a cache of its
|
|
792
|
+
// state, refreshed on selection change and kept live by the
|
|
793
|
+
// `tis.cycle_config` broadcast. Every mutation round-trips and adopts
|
|
794
|
+
// the reply, so the HMI can't drift from the `tis.cycle_ready` scalar
|
|
795
|
+
// the machine is actually gated on.
|
|
796
|
+
// -----------------------------------------------------------------
|
|
797
|
+
const [cycleConfig, setCycleConfigStateRaw] = useState<TisCycleConfigState>(EMPTY_CYCLE_CONFIG);
|
|
798
|
+
|
|
799
|
+
const cycleConfigFields = useMemo<TestFieldDef[]>(() => {
|
|
800
|
+
const arr = selection.methodId
|
|
801
|
+
? schemas[selection.methodId]?.cycle_config_fields
|
|
802
|
+
: undefined;
|
|
803
|
+
return Array.isArray(arr) ? arr : [];
|
|
804
|
+
}, [schemas, selection.methodId]);
|
|
805
|
+
|
|
806
|
+
const refreshCycleConfig = useCallback(async () => {
|
|
807
|
+
const projectId = selection.projectId;
|
|
808
|
+
const methodId = selection.methodId;
|
|
809
|
+
if (!methodId) { setCycleConfigStateRaw(EMPTY_CYCLE_CONFIG); return; }
|
|
810
|
+
try {
|
|
811
|
+
const resp: any = await invoke('tis.cycle_config' as any, MessageType.Request, {
|
|
812
|
+
project_id: projectId, method_id: methodId,
|
|
813
|
+
} as any);
|
|
814
|
+
if (resp?.success) {
|
|
815
|
+
setCycleConfigStateRaw(cycleConfigFromPayload(resp.data));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
// An older server without the verb, or a method the project
|
|
819
|
+
// doesn't declare. Either way there is nothing to gate on.
|
|
820
|
+
setCycleConfigStateRaw(EMPTY_CYCLE_CONFIG);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
console.warn('[TisProvider] tis.cycle_config failed:', e);
|
|
823
|
+
setCycleConfigStateRaw(EMPTY_CYCLE_CONFIG);
|
|
824
|
+
}
|
|
825
|
+
}, [invoke, selection.projectId, selection.methodId]);
|
|
826
|
+
|
|
827
|
+
const setCycleConfigValues = useCallback(async (patch: Record<string, any>) => {
|
|
828
|
+
const projectId = selection.projectId;
|
|
829
|
+
const methodId = selection.methodId;
|
|
830
|
+
if (!methodId) return;
|
|
831
|
+
try {
|
|
832
|
+
const resp: any = await invoke('tis.set_cycle_config' as any, MessageType.Request, {
|
|
833
|
+
project_id: projectId, method_id: methodId, values: patch,
|
|
834
|
+
} as any);
|
|
835
|
+
if (resp?.success) setCycleConfigStateRaw(cycleConfigFromPayload(resp.data));
|
|
836
|
+
else console.warn('[TisProvider] tis.set_cycle_config rejected:', resp?.error_message);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
console.error('[TisProvider] tis.set_cycle_config failed:', e);
|
|
839
|
+
}
|
|
840
|
+
}, [invoke, selection.projectId, selection.methodId]);
|
|
841
|
+
|
|
842
|
+
const clearCycleConfig = useCallback(async () => {
|
|
843
|
+
const projectId = selection.projectId;
|
|
844
|
+
const methodId = selection.methodId;
|
|
845
|
+
if (!methodId) return;
|
|
846
|
+
try {
|
|
847
|
+
const resp: any = await invoke('tis.clear_cycle_config' as any, MessageType.Request, {
|
|
848
|
+
project_id: projectId, method_id: methodId,
|
|
849
|
+
} as any);
|
|
850
|
+
if (resp?.success) setCycleConfigStateRaw(cycleConfigFromPayload(resp.data));
|
|
851
|
+
} catch (e) {
|
|
852
|
+
console.error('[TisProvider] tis.clear_cycle_config failed:', e);
|
|
853
|
+
}
|
|
854
|
+
}, [invoke, selection.projectId, selection.methodId]);
|
|
855
|
+
|
|
856
|
+
useEffect(() => { void refreshCycleConfig(); }, [refreshCycleConfig]);
|
|
857
|
+
|
|
858
|
+
useEffect(() => {
|
|
859
|
+
// Server-pushed state: latch taken, autoclear fired, gate re-armed.
|
|
860
|
+
// Payloads for another method are ignored so a multi-method HMI
|
|
861
|
+
// doesn't show one method's annotation while another is selected.
|
|
862
|
+
const onCycleConfig = (payload: any) => {
|
|
863
|
+
const mid = String(payload?.method_id ?? '');
|
|
864
|
+
if (mid && selection.methodId && mid !== selection.methodId) return;
|
|
865
|
+
setCycleConfigStateRaw(cycleConfigFromPayload(payload));
|
|
866
|
+
};
|
|
867
|
+
const id = subscribe('tis.cycle_config', onCycleConfig);
|
|
868
|
+
return () => { unsubscribe(id); };
|
|
869
|
+
}, [subscribe, unsubscribe, selection.methodId]);
|
|
870
|
+
|
|
702
871
|
const value: TisContextValue = useMemo(() => ({
|
|
703
872
|
schemas, projectAssetRefs, defaultMethodId, schemasLoaded,
|
|
704
873
|
state, selection, setSelection,
|
|
@@ -707,6 +876,8 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
707
876
|
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
708
877
|
defaultsAppliedForMethod, markDefaultsAppliedForMethod,
|
|
709
878
|
configurationName, setConfigurationName,
|
|
879
|
+
cycleConfig, cycleConfigFields,
|
|
880
|
+
setCycleConfigValues, clearCycleConfig, refreshCycleConfig,
|
|
710
881
|
fetchRuns, fetchRun, runCache,
|
|
711
882
|
}), [
|
|
712
883
|
schemas, projectAssetRefs, defaultMethodId, schemasLoaded,
|
|
@@ -716,6 +887,8 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
716
887
|
stagedConfig, setStagedConfig, clearStagedConfig,
|
|
717
888
|
defaultsAppliedForMethod, markDefaultsAppliedForMethod,
|
|
718
889
|
configurationName, setConfigurationName,
|
|
890
|
+
cycleConfig, cycleConfigFields,
|
|
891
|
+
setCycleConfigValues, clearCycleConfig, refreshCycleConfig,
|
|
719
892
|
fetchRuns, fetchRun, runCache,
|
|
720
893
|
]);
|
|
721
894
|
|
|
@@ -729,6 +902,24 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
729
902
|
export const useTis = () => useContext(TisContext);
|
|
730
903
|
export const useTisSchemas = () => useContext(TisContext).schemas;
|
|
731
904
|
export const useTisState = () => useContext(TisContext).state;
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Cycle Configuration for the selected method: the live state, the field
|
|
908
|
+
* declarations, and the two mutators.
|
|
909
|
+
*
|
|
910
|
+
* `fields.length === 0` means the method doesn't use per-row annotation —
|
|
911
|
+
* hide the Cycle tab and the status strip rather than showing an empty one.
|
|
912
|
+
*/
|
|
913
|
+
export const useTisCycleConfig = () => {
|
|
914
|
+
const { cycleConfig, cycleConfigFields, setCycleConfigValues, clearCycleConfig } =
|
|
915
|
+
useContext(TisContext);
|
|
916
|
+
return {
|
|
917
|
+
...cycleConfig,
|
|
918
|
+
fields: cycleConfigFields,
|
|
919
|
+
setValues: setCycleConfigValues,
|
|
920
|
+
clear: clearCycleConfig,
|
|
921
|
+
};
|
|
922
|
+
};
|
|
732
923
|
/**
|
|
733
924
|
* Tuple of `[selection, setSelection]`.
|
|
734
925
|
*
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CycleConfigSection — the two method-level settings that decide how the
|
|
3
|
+
* operator's per-row annotation behaves. The columns themselves are
|
|
4
|
+
* authored in the Fields tab; this is the state machine around them.
|
|
5
|
+
*
|
|
6
|
+
* The scope choice is the load-bearing one. A machine that records many
|
|
7
|
+
* cycles from one Start is not pausing for input between them, so its
|
|
8
|
+
* annotation has to latch once per Start ('run'). A machine the operator
|
|
9
|
+
* drives one cycle at a time — the six-position sweep — annotates each row
|
|
10
|
+
* separately ('cycle'), which means the gate re-arms between cycles and
|
|
11
|
+
* the control program must wait on `tis.cycle_ready`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
15
|
+
import { Checkbox } from 'primereact/checkbox';
|
|
16
|
+
import { FormSection } from '../../forms/FormSection';
|
|
17
|
+
import { FormRow } from '../../forms/FormRow';
|
|
18
|
+
import type { CycleConfigScope, TestField, TestMethod } from '../types';
|
|
19
|
+
|
|
20
|
+
const SCOPES: { label: string; value: CycleConfigScope }[] = [
|
|
21
|
+
{ label: 'run — one entry per Start, stamped on every cycle of the run', value: 'run' },
|
|
22
|
+
{ label: 'cycle — one entry per cycle, re-entered between cycles', value: 'cycle' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export interface CycleConfigSectionProps {
|
|
26
|
+
method: TestMethod;
|
|
27
|
+
onChange: (next: TestMethod) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const CycleConfigSection: React.FC<CycleConfigSectionProps> = ({ method, onChange }) => {
|
|
31
|
+
const fields = (method.cycle_config_fields as TestField[]) ?? [];
|
|
32
|
+
const scope: CycleConfigScope = method.cycle_config_scope === 'cycle' ? 'cycle' : 'run';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Write a key only when it differs from the server-side default,
|
|
36
|
+
* deleting it otherwise. Both of these are `#[serde(default)]` in Rust,
|
|
37
|
+
* so materialising `"cycle_config_scope": "run"` into every method would
|
|
38
|
+
* add noise to test_methods.json that means nothing — and an editor that
|
|
39
|
+
* writes defaults it was only shown is how a review session ends up
|
|
40
|
+
* changing methods nobody touched.
|
|
41
|
+
*/
|
|
42
|
+
const setOrDrop = <K extends 'cycle_config_scope' | 'prompt_before_cycle'>(
|
|
43
|
+
key: K, value: TestMethod[K], isDefault: boolean,
|
|
44
|
+
) => {
|
|
45
|
+
const next: TestMethod = { ...method };
|
|
46
|
+
if (isDefault) delete next[key];
|
|
47
|
+
else next[key] = value;
|
|
48
|
+
onChange(next);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<FormSection
|
|
53
|
+
title="Cycle Configuration"
|
|
54
|
+
description={
|
|
55
|
+
'How the operator-entered annotation columns behave. Add the columns themselves ' +
|
|
56
|
+
'on the Fields tab under "Cycle Configuration Fields".'
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
{fields.length === 0 && (
|
|
60
|
+
<div style={{ color: '#6b7280', marginBottom: '0.75rem' }}>
|
|
61
|
+
This method declares no Cycle Configuration fields, so these settings have no effect.
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
<FormRow
|
|
65
|
+
label="Scope"
|
|
66
|
+
hint={
|
|
67
|
+
'When the operator\'s entry is latched and cleared. Choose "cycle" only if the ' +
|
|
68
|
+
'machine actually waits between cycles — under "cycle" the gate drops after every ' +
|
|
69
|
+
'row and the control program must hold until tis.cycle_ready comes back true.'
|
|
70
|
+
}
|
|
71
|
+
>
|
|
72
|
+
<Dropdown
|
|
73
|
+
value={scope}
|
|
74
|
+
options={SCOPES}
|
|
75
|
+
onChange={(e) => setOrDrop('cycle_config_scope', e.value, e.value === 'run')}
|
|
76
|
+
/>
|
|
77
|
+
</FormRow>
|
|
78
|
+
<FormRow
|
|
79
|
+
label="Prompt before cycle"
|
|
80
|
+
hint={
|
|
81
|
+
'Open the entry dialog at the boundary instead of relying on the operator having ' +
|
|
82
|
+
'filled the Cycle tab. Costs a confirmation each time — worth it when a mislabelled ' +
|
|
83
|
+
'row means re-running the sample, not worth it for a non-critical note.'
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
<Checkbox
|
|
87
|
+
checked={!!method.prompt_before_cycle}
|
|
88
|
+
onChange={(e) => setOrDrop('prompt_before_cycle', !!e.checked, !e.checked)}
|
|
89
|
+
/>
|
|
90
|
+
</FormRow>
|
|
91
|
+
</FormSection>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
@@ -9,6 +9,7 @@ import type { TestField, TestMethod, FieldArrayKey } from '../types';
|
|
|
9
9
|
const TITLES: Record<FieldArrayKey, string> = {
|
|
10
10
|
project_fields: 'Project Fields',
|
|
11
11
|
config_fields: 'Config Fields',
|
|
12
|
+
cycle_config_fields: 'Cycle Configuration Fields',
|
|
12
13
|
cycle_fields: 'Cycle Fields',
|
|
13
14
|
results_fields: 'Results Fields',
|
|
14
15
|
};
|
|
@@ -16,6 +17,7 @@ const TITLES: Record<FieldArrayKey, string> = {
|
|
|
16
17
|
const DESCRIPTIONS: Record<FieldArrayKey, string> = {
|
|
17
18
|
project_fields: 'System-level fields (operator, station). Filled by the HMI but not cycled.',
|
|
18
19
|
config_fields: 'Operator-input config (speeds, loads). Snapshotted into test.json on start.',
|
|
20
|
+
cycle_config_fields: 'Operator-entered annotation recorded onto every cycle row (e.g. which of six positions this is). Shown on the HMI Cycle tab and written as the first Cycle Data columns after the row index. Mark them Required so the machine cannot start unannotated, and Autoclear so the next row cannot inherit a stale value.',
|
|
19
21
|
cycle_fields: 'Per-cycle capture. One row appended to cycles.jsonl per cycle.',
|
|
20
22
|
results_fields: 'Post-test summary (min/max/avg, pass/fail). Written once at finish. A field can also be a cross-test Aggregate (avg/min/max/stddev/count over tests sharing a sample_id).',
|
|
21
23
|
};
|
|
@@ -130,6 +132,13 @@ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, me
|
|
|
130
132
|
style={{ width: '8rem' }}
|
|
131
133
|
/>
|
|
132
134
|
)}
|
|
135
|
+
{arrayKey === 'cycle_config_fields' && (
|
|
136
|
+
<Column
|
|
137
|
+
header="Autoclear"
|
|
138
|
+
body={(r: TestField) => r.autoclear ? '✓' : ''}
|
|
139
|
+
style={{ width: '6rem' }}
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
133
142
|
<Column header="" body={rowActions} style={{ width: '10rem' }} />
|
|
134
143
|
</DataTable>
|
|
135
144
|
</FormSection>
|
|
@@ -138,6 +147,7 @@ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, me
|
|
|
138
147
|
initial={editingIdx !== null ? (fields[editingIdx] ?? null) : null}
|
|
139
148
|
siblingNames={fields.map(f => f.name)}
|
|
140
149
|
isResultsField={arrayKey === 'results_fields'}
|
|
150
|
+
isCycleConfigField={arrayKey === 'cycle_config_fields'}
|
|
141
151
|
cycleFieldNames={(method.cycle_fields ?? []).map(f => f.name).filter(Boolean)}
|
|
142
152
|
resultsFieldNames={(method.results_fields ?? []).map(f => f.name).filter(Boolean)}
|
|
143
153
|
onCancel={() => setDialogOpen(false)}
|
|
@@ -13,6 +13,7 @@ import { Button } from 'primereact/button';
|
|
|
13
13
|
import { CodeEditor } from '../../CodeEditor';
|
|
14
14
|
import { IdentitySection } from './IdentitySection';
|
|
15
15
|
import { FieldArrayEditor } from './FieldArrayEditor';
|
|
16
|
+
import { CycleConfigSection } from './CycleConfigSection';
|
|
16
17
|
import { ConfigurationsEditor } from './ConfigurationsEditor';
|
|
17
18
|
import { ViewsEditor } from './ViewsEditor';
|
|
18
19
|
import { RawDataEditor } from './RawDataEditor';
|
|
@@ -104,7 +105,6 @@ export const MethodFormEditor: React.FC<MethodFormEditorProps> = ({
|
|
|
104
105
|
{dirty && <span style={{ marginLeft: '0.5rem', color: '#ea580c' }}>•</span>}
|
|
105
106
|
{validationErrors.length > 0 && (
|
|
106
107
|
<span
|
|
107
|
-
title={validationErrors.map(e => e.message).join('\n')}
|
|
108
108
|
style={{
|
|
109
109
|
marginLeft: '0.5rem',
|
|
110
110
|
background: '#fef2f2',
|
|
@@ -113,7 +113,6 @@ export const MethodFormEditor: React.FC<MethodFormEditorProps> = ({
|
|
|
113
113
|
borderRadius: 4,
|
|
114
114
|
fontSize: '0.75rem',
|
|
115
115
|
fontWeight: 600,
|
|
116
|
-
cursor: 'help',
|
|
117
116
|
}}
|
|
118
117
|
>
|
|
119
118
|
{validationErrors.length} issue{validationErrors.length === 1 ? '' : 's'}
|
|
@@ -130,16 +129,49 @@ export const MethodFormEditor: React.FC<MethodFormEditorProps> = ({
|
|
|
130
129
|
</div>
|
|
131
130
|
</div>
|
|
132
131
|
|
|
132
|
+
{/* Validation issues, spelled out. These disable Apply, so the
|
|
133
|
+
operator has to be able to READ them — the count used to live
|
|
134
|
+
in a hover `title`, which on the touchscreen HMIs this ships
|
|
135
|
+
to is no affordance at all: Apply was greyed out and nothing
|
|
136
|
+
on screen said why. (gen4_ac, 2026-08-12: a method whose only
|
|
137
|
+
problem was a false-positive chart-axis error couldn't be
|
|
138
|
+
edited at all, and the reason was invisible.) */}
|
|
139
|
+
{validationErrors.length > 0 && (
|
|
140
|
+
<div
|
|
141
|
+
style={{
|
|
142
|
+
padding: '0.5rem 1rem',
|
|
143
|
+
background: '#fef2f2',
|
|
144
|
+
color: '#991b1b',
|
|
145
|
+
borderBottom: '1px solid #fecaca',
|
|
146
|
+
fontSize: '0.85rem',
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
<strong>
|
|
150
|
+
Fix {validationErrors.length === 1 ? 'this issue' : 'these issues'} before applying:
|
|
151
|
+
</strong>
|
|
152
|
+
<ul style={{ margin: '0.25rem 0 0 1.25rem' }}>
|
|
153
|
+
{validationErrors.map((e, i) => (
|
|
154
|
+
<li key={i}>{e.message}</li>
|
|
155
|
+
))}
|
|
156
|
+
</ul>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
133
160
|
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
|
134
161
|
<TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}>
|
|
135
162
|
<TabPanel header="Identity">
|
|
136
163
|
<IdentitySection method={draft} onChange={handleFormChange} />
|
|
137
164
|
</TabPanel>
|
|
138
165
|
<TabPanel header="Fields">
|
|
139
|
-
<FieldArrayEditor arrayKey="project_fields"
|
|
140
|
-
<FieldArrayEditor arrayKey="config_fields"
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
<FieldArrayEditor arrayKey="project_fields" method={draft} onChange={handleFormChange} />
|
|
167
|
+
<FieldArrayEditor arrayKey="config_fields" method={draft} onChange={handleFormChange} />
|
|
168
|
+
{/* Cycle Configuration sits directly above Cycle
|
|
169
|
+
Fields: the two together are one row, annotation
|
|
170
|
+
columns first. */}
|
|
171
|
+
<FieldArrayEditor arrayKey="cycle_config_fields" method={draft} onChange={handleFormChange} />
|
|
172
|
+
<CycleConfigSection method={draft} onChange={handleFormChange} />
|
|
173
|
+
<FieldArrayEditor arrayKey="cycle_fields" method={draft} onChange={handleFormChange} />
|
|
174
|
+
<FieldArrayEditor arrayKey="results_fields" method={draft} onChange={handleFormChange} />
|
|
143
175
|
</TabPanel>
|
|
144
176
|
<TabPanel header="Configurations">
|
|
145
177
|
<ConfigurationsEditor method={draft} onChange={handleFormChange} />
|
|
@@ -45,6 +45,9 @@ export interface TestFieldDialogProps {
|
|
|
45
45
|
/** True when this field belongs to `results_fields` — only then is the
|
|
46
46
|
* cross-test Aggregate section shown. */
|
|
47
47
|
isResultsField?: boolean;
|
|
48
|
+
/** True when this field belongs to `cycle_config_fields` — only then is
|
|
49
|
+
* the Autoclear option shown, and only there does it do anything. */
|
|
50
|
+
isCycleConfigField?: boolean;
|
|
48
51
|
/** Candidate target field names for an aggregate's `of`, by source. */
|
|
49
52
|
cycleFieldNames?: string[];
|
|
50
53
|
resultsFieldNames?: string[];
|
|
@@ -54,7 +57,8 @@ const blank: TestField = { name: '', type: 'f32' };
|
|
|
54
57
|
|
|
55
58
|
export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
56
59
|
visible, initial, onCancel, onSave, siblingNames,
|
|
57
|
-
isResultsField = false,
|
|
60
|
+
isResultsField = false, isCycleConfigField = false,
|
|
61
|
+
cycleFieldNames = [], resultsFieldNames = [],
|
|
58
62
|
}) => {
|
|
59
63
|
const [draft, setDraft] = useState<TestField>(blank);
|
|
60
64
|
// `default` is edited as free text so a numeric literal, a string, or an
|
|
@@ -162,12 +166,28 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
162
166
|
onChange={(e) => setDraft({ ...draft, label: e.target.value || undefined })}
|
|
163
167
|
/>
|
|
164
168
|
</FormRow>
|
|
165
|
-
<FormRow
|
|
169
|
+
<FormRow
|
|
170
|
+
label="Required"
|
|
171
|
+
hint={isCycleConfigField
|
|
172
|
+
? 'Blocks the machine until the operator fills this in — tis.cycle_ready stays false and start_test is refused. This is what stops an unannotated row from being recorded.'
|
|
173
|
+
: undefined}
|
|
174
|
+
>
|
|
166
175
|
<Checkbox
|
|
167
176
|
checked={!!draft.required}
|
|
168
177
|
onChange={(e) => setDraft({ ...draft, required: !!e.checked })}
|
|
169
178
|
/>
|
|
170
179
|
</FormRow>
|
|
180
|
+
{isCycleConfigField && (
|
|
181
|
+
<FormRow
|
|
182
|
+
label="Autoclear"
|
|
183
|
+
hint="Clear the operator's entry once a row has recorded it, so the next row can't inherit a stale value. Fires after each cycle under 'cycle' scope, at the end of the run under 'run' scope. Pair with Required — on its own it turns stale data into silently missing data."
|
|
184
|
+
>
|
|
185
|
+
<Checkbox
|
|
186
|
+
checked={!!draft.autoclear}
|
|
187
|
+
onChange={(e) => setDraft({ ...draft, autoclear: !!e.checked })}
|
|
188
|
+
/>
|
|
189
|
+
</FormRow>
|
|
190
|
+
)}
|
|
171
191
|
<FormRow label="Source" hint="Optional gm.* variable to bind this field to.">
|
|
172
192
|
<InputText
|
|
173
193
|
value={draft.source ?? ''}
|
|
@@ -31,9 +31,16 @@ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) =>
|
|
|
31
31
|
const knownKeys: string[] = [
|
|
32
32
|
...((method.project_fields ?? []) as TestField[]).map(f => f.name),
|
|
33
33
|
...((method.config_fields ?? []) as TestField[]).map(f => f.name),
|
|
34
|
+
// Annotation columns live on the cycle row like any measured one, so
|
|
35
|
+
// a cycle_scatter can plot against them (e.g. peak load by position).
|
|
36
|
+
...((method.cycle_config_fields ?? []) as TestField[]).map(f => f.name),
|
|
34
37
|
...((method.cycle_fields ?? []) as TestField[]).map(f => f.name),
|
|
35
38
|
...((method.results_fields ?? []) as TestField[]).map(f => f.name),
|
|
36
39
|
...Object.keys((method.raw_data as any)?.columns ?? {}),
|
|
40
|
+
// Written onto every row by the server, not declared anywhere —
|
|
41
|
+
// offered here so "peak load by cycle" is a pick, not a guess.
|
|
42
|
+
'cycle_index',
|
|
43
|
+
'timestamp',
|
|
37
44
|
];
|
|
38
45
|
|
|
39
46
|
const openNew = () => { setEditingId(null); setDialogOpen(true); };
|