@adcops/autocore-react 3.3.113 → 3.3.116
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/ResultHistoryTable.d.ts.map +1 -1
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/ScienceTable.d.ts +15 -0
- package/dist/components/tis/ScienceTable.d.ts.map +1 -1
- package/dist/components/tis/ScienceTable.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +14 -4
- 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 +110 -110
- 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/ResultHistoryTable.tsx +141 -1
- package/src/components/tis/ScienceTable.tsx +71 -12
- package/src/components/tis/TestDataView.tsx +82 -7
- 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 +10 -4
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +22 -2
- package/src/components/tis-editor/editor/ViewsEditor.tsx +3 -0
- package/src/components/tis-editor/types.ts +34 -1
- package/src/components/tis-editor/validation.ts +20 -2
|
@@ -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';
|
|
@@ -136,10 +137,15 @@ export const MethodFormEditor: React.FC<MethodFormEditorProps> = ({
|
|
|
136
137
|
<IdentitySection method={draft} onChange={handleFormChange} />
|
|
137
138
|
</TabPanel>
|
|
138
139
|
<TabPanel header="Fields">
|
|
139
|
-
<FieldArrayEditor arrayKey="project_fields"
|
|
140
|
-
<FieldArrayEditor arrayKey="config_fields"
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
<FieldArrayEditor arrayKey="project_fields" method={draft} onChange={handleFormChange} />
|
|
141
|
+
<FieldArrayEditor arrayKey="config_fields" method={draft} onChange={handleFormChange} />
|
|
142
|
+
{/* Cycle Configuration sits directly above Cycle
|
|
143
|
+
Fields: the two together are one row, annotation
|
|
144
|
+
columns first. */}
|
|
145
|
+
<FieldArrayEditor arrayKey="cycle_config_fields" method={draft} onChange={handleFormChange} />
|
|
146
|
+
<CycleConfigSection method={draft} onChange={handleFormChange} />
|
|
147
|
+
<FieldArrayEditor arrayKey="cycle_fields" method={draft} onChange={handleFormChange} />
|
|
148
|
+
<FieldArrayEditor arrayKey="results_fields" method={draft} onChange={handleFormChange} />
|
|
143
149
|
</TabPanel>
|
|
144
150
|
<TabPanel header="Configurations">
|
|
145
151
|
<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,6 +31,9 @@ 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 ?? {}),
|
|
@@ -31,6 +31,15 @@ export interface TestField {
|
|
|
31
31
|
* non-excluded runs sharing this test's sample_id. When set, the field
|
|
32
32
|
* is NOT supplied by the analysis script — the server computes it. */
|
|
33
33
|
aggregate?: AggregateSpec;
|
|
34
|
+
/** cycle_fields only: operator-correctable after recording. Measured
|
|
35
|
+
* columns must stay unflagged. cycle_config_fields don't need it —
|
|
36
|
+
* they're annotation and are always correctable. */
|
|
37
|
+
editable?: boolean;
|
|
38
|
+
/** cycle_config_fields only: clear the operator's entry once a row has
|
|
39
|
+
* recorded it, so the next row can't inherit a stale annotation. Pair
|
|
40
|
+
* it with `required` — on its own it turns stale data into silently
|
|
41
|
+
* missing data. */
|
|
42
|
+
autoclear?: boolean;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export type AggFn = 'avg' | 'min' | 'max' | 'stddev' | 'count';
|
|
@@ -113,11 +122,30 @@ export interface TestConfiguration {
|
|
|
113
122
|
defaults?: Record<string, unknown>;
|
|
114
123
|
}
|
|
115
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Which boundary an operator annotates `cycle_config_fields` at.
|
|
127
|
+
*
|
|
128
|
+
* 'run' one latch per Start — the same values stamp every cycle of the
|
|
129
|
+
* run and clear when it closes. Default; correct for a machine
|
|
130
|
+
* that records many cycles from one Start.
|
|
131
|
+
* 'cycle' one latch per cycle — values clear after each recorded row and
|
|
132
|
+
* the gate re-arms, so every row is annotated separately.
|
|
133
|
+
*/
|
|
134
|
+
export type CycleConfigScope = 'run' | 'cycle';
|
|
135
|
+
|
|
116
136
|
export interface TestMethod {
|
|
117
137
|
label?: string;
|
|
118
138
|
description?: string;
|
|
119
139
|
project_fields?: TestField[];
|
|
120
140
|
config_fields?: TestField[];
|
|
141
|
+
/** Operator-entered per-row annotation, recorded as the first Cycle Data
|
|
142
|
+
* columns after the row index. */
|
|
143
|
+
cycle_config_fields?: TestField[];
|
|
144
|
+
/** Boundary at which cycle_config_fields latch and autoclear. */
|
|
145
|
+
cycle_config_scope?: CycleConfigScope;
|
|
146
|
+
/** Open the Cycle Configuration dialog at the boundary rather than
|
|
147
|
+
* relying on the operator having filled the Cycle tab. */
|
|
148
|
+
prompt_before_cycle?: boolean;
|
|
121
149
|
cycle_fields?: TestField[];
|
|
122
150
|
results_fields?: TestField[];
|
|
123
151
|
raw_data?: RawDataShape | null;
|
|
@@ -128,4 +156,9 @@ export interface TestMethod {
|
|
|
128
156
|
[key: string]: unknown; // tolerate unknown server-side fields
|
|
129
157
|
}
|
|
130
158
|
|
|
131
|
-
export type FieldArrayKey =
|
|
159
|
+
export type FieldArrayKey =
|
|
160
|
+
| 'project_fields'
|
|
161
|
+
| 'config_fields'
|
|
162
|
+
| 'cycle_config_fields'
|
|
163
|
+
| 'cycle_fields'
|
|
164
|
+
| 'results_fields';
|
|
@@ -10,7 +10,9 @@ import type {
|
|
|
10
10
|
TestField, TestMethod, ChartAxis, ChartSeries,
|
|
11
11
|
} from './types';
|
|
12
12
|
|
|
13
|
-
const FIELD_ARRAY_KEYS = [
|
|
13
|
+
const FIELD_ARRAY_KEYS = [
|
|
14
|
+
'project_fields', 'config_fields', 'cycle_config_fields', 'cycle_fields', 'results_fields',
|
|
15
|
+
] as const;
|
|
14
16
|
|
|
15
17
|
export interface ValidationError {
|
|
16
18
|
/** Dotted path: "<arrayKey>.<index>.<sub>" or "views.<viewId>.x" etc. */
|
|
@@ -49,7 +51,7 @@ export function validateMethod(methodId: string, m: TestMethod): ValidationError
|
|
|
49
51
|
const resultTypes = new Map<string, string>(
|
|
50
52
|
((m.results_fields as TestField[] | undefined) ?? []).map((f) => [f.name, String(f.type)])
|
|
51
53
|
);
|
|
52
|
-
for (const key of ['project_fields', 'config_fields', 'cycle_fields'] as const) {
|
|
54
|
+
for (const key of ['project_fields', 'config_fields', 'cycle_config_fields', 'cycle_fields'] as const) {
|
|
53
55
|
((m[key] as TestField[] | undefined) ?? []).forEach((f, i) => {
|
|
54
56
|
if (f.aggregate) {
|
|
55
57
|
errs.push({ path: `${key}.${i}.aggregate`, message: `${methodId}.${key}.${f.name}: aggregate is only valid on results_fields` });
|
|
@@ -74,6 +76,22 @@ export function validateMethod(methodId: string, m: TestMethod): ValidationError
|
|
|
74
76
|
}
|
|
75
77
|
});
|
|
76
78
|
|
|
79
|
+
// A cycle_config_field and a cycle_field share one row namespace in
|
|
80
|
+
// cycles.jsonl — the same name in both means the operator's annotation
|
|
81
|
+
// and a measured value would fight over one column. Mirrors the
|
|
82
|
+
// server-side check in validate_method.
|
|
83
|
+
const cycleFieldNames = new Set<string>(
|
|
84
|
+
((m.cycle_fields as TestField[] | undefined) ?? []).map(f => f.name).filter(Boolean),
|
|
85
|
+
);
|
|
86
|
+
((m.cycle_config_fields as TestField[] | undefined) ?? []).forEach((f, i) => {
|
|
87
|
+
if (f.name && cycleFieldNames.has(f.name)) {
|
|
88
|
+
errs.push({
|
|
89
|
+
path: `cycle_config_fields.${i}.name`,
|
|
90
|
+
message: `${methodId}: "${f.name}" is declared as both a cycle_config_field and a cycle_field. They share one row namespace; rename one.`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
77
95
|
// Build sets for axis-reference resolution.
|
|
78
96
|
const knownFields = new Set<string>();
|
|
79
97
|
for (const key of FIELD_ARRAY_KEYS) {
|