@adcops/autocore-react 3.3.106 → 3.3.111
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/assets/JogXLeft.d.ts +4 -0
- package/dist/assets/JogXLeft.d.ts.map +1 -0
- package/dist/assets/JogXLeft.js +1 -0
- package/dist/assets/JogXRight.d.ts +4 -0
- package/dist/assets/JogXRight.d.ts.map +1 -0
- package/dist/assets/JogXRight.js +1 -0
- package/dist/assets/index.d.ts +2 -2
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -1
- package/dist/components/JogPanel.d.ts +29 -4
- package/dist/components/JogPanel.d.ts.map +1 -1
- package/dist/components/JogPanel.js +1 -1
- package/dist/components/ValueInput.d.ts.map +1 -1
- package/dist/components/ValueInput.js +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/network/StagedChangeBanner.d.ts.map +1 -1
- package/dist/components/network/StagedChangeBanner.js +1 -1
- package/dist/components/tis/ResultHistoryTable.css +12 -0
- package/dist/components/tis/ResultHistoryTable.d.ts +1 -0
- package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/SampleSummaryPanel.d.ts +23 -0
- package/dist/components/tis/SampleSummaryPanel.d.ts.map +1 -0
- package/dist/components/tis/SampleSummaryPanel.js +1 -0
- package/dist/components/tis/ScienceTable.css +87 -0
- package/dist/components/tis/ScienceTable.d.ts +35 -0
- package/dist/components/tis/ScienceTable.d.ts.map +1 -0
- package/dist/components/tis/ScienceTable.js +1 -0
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.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-editor/editor/FieldArrayEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +6 -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/types.d.ts +15 -0
- 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/assets/{JogXNeg.tsx → JogXLeft.tsx} +6 -3
- package/src/assets/{JogXPos.tsx → JogXRight.tsx} +6 -3
- package/src/assets/index.ts +2 -2
- package/src/components/JogPanel.tsx +64 -59
- package/src/components/ValueInput.tsx +10 -3
- package/src/components/index.ts +3 -0
- package/src/components/network/StagedChangeBanner.tsx +22 -3
- package/src/components/tis/ResultHistoryTable.css +12 -0
- package/src/components/tis/ResultHistoryTable.tsx +76 -4
- package/src/components/tis/SampleSummaryPanel.tsx +171 -0
- package/src/components/tis/ScienceTable.css +87 -0
- package/src/components/tis/ScienceTable.tsx +114 -0
- package/src/components/tis/TestDataView.tsx +92 -51
- package/src/components/tis/TestSetupForm.tsx +18 -2
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +18 -1
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +82 -1
- package/src/components/tis-editor/types.ts +17 -0
- package/src/components/tis-editor/validation.ts +35 -0
- package/dist/assets/JogXNeg.d.ts +0 -4
- package/dist/assets/JogXNeg.d.ts.map +0 -1
- package/dist/assets/JogXNeg.js +0 -1
- package/dist/assets/JogXPos.d.ts +0 -4
- package/dist/assets/JogXPos.d.ts.map +0 -1
- package/dist/assets/JogXPos.js +0 -1
|
@@ -44,6 +44,8 @@ import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
|
44
44
|
import { MessageType } from '../../hub/CommandMessage';
|
|
45
45
|
import { useTis } from './TisProvider';
|
|
46
46
|
import { useRawCycleData } from './useRawCycleData';
|
|
47
|
+
import { ScienceTable } from './ScienceTable';
|
|
48
|
+
import type { ScienceColumn } from './ScienceTable';
|
|
47
49
|
|
|
48
50
|
ChartJS.register(
|
|
49
51
|
CategoryScale, LinearScale, PointElement, LineElement,
|
|
@@ -199,6 +201,10 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
199
201
|
const [meta, setMeta] = useState<any>(null);
|
|
200
202
|
const [cycles, setCycles] = useState<any[]>([]);
|
|
201
203
|
const [results, setResults] = useState<any>({});
|
|
204
|
+
// Server-derived cross-test aggregate results_fields (avg/min/max/stddev/
|
|
205
|
+
// count over the sample group). Overlaid on `results` for display — these
|
|
206
|
+
// fields are never in the per-test results blob.
|
|
207
|
+
const [sampleSummary, setSampleSummary] = useState<any>({});
|
|
202
208
|
const [rawOpen, setRawOpen] = useState(false);
|
|
203
209
|
const [configOpen, setConfigOpen] = useState(false);
|
|
204
210
|
|
|
@@ -351,6 +357,45 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
351
357
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
352
358
|
}, [projectId, methodId, runId, throttleMs]);
|
|
353
359
|
|
|
360
|
+
// -----------------------------------------------------------------
|
|
361
|
+
// Cross-test aggregate summary (sample-group rollup)
|
|
362
|
+
//
|
|
363
|
+
// Keyed by sample_id, not run_id: every run of the same sample shares
|
|
364
|
+
// the rollup. Fetched once per sample and refreshed on the server's
|
|
365
|
+
// sample_summary_updated broadcast (fired when a sibling finishes or its
|
|
366
|
+
// exclusion is toggled). Empty unless the method declares aggregate
|
|
367
|
+
// results_fields.
|
|
368
|
+
// -----------------------------------------------------------------
|
|
369
|
+
const sampleId: string =
|
|
370
|
+
(typeof meta?.sample_id === 'string' && meta.sample_id)
|
|
371
|
+
? meta.sample_id
|
|
372
|
+
: (typeof meta?.config?.sample_id === 'string' ? meta.config.sample_id : '');
|
|
373
|
+
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (!projectId || !methodId || !sampleId) { setSampleSummary({}); return; }
|
|
376
|
+
let cancelled = false;
|
|
377
|
+
const fetchSummary = async () => {
|
|
378
|
+
try {
|
|
379
|
+
const resp: any = await invoke(
|
|
380
|
+
'tis.sample_summary' as any, MessageType.Request as any,
|
|
381
|
+
{ project_id: projectId, method_id: methodId, sample_id: sampleId } as any);
|
|
382
|
+
if (!cancelled && resp?.success) setSampleSummary(resp.data?.summary ?? {});
|
|
383
|
+
} catch (e) {
|
|
384
|
+
console.error('[TestDataView] sample_summary fetch failed', e);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
fetchSummary();
|
|
388
|
+
const onSummary = (payload: any) => {
|
|
389
|
+
if (payload?.project_id === projectId
|
|
390
|
+
&& payload?.method_id === methodId
|
|
391
|
+
&& payload?.sample_id === sampleId) {
|
|
392
|
+
setSampleSummary(payload.summary ?? {});
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const id = subscribe('tis.sample_summary_updated', onSummary);
|
|
396
|
+
return () => { cancelled = true; unsubscribe(id); };
|
|
397
|
+
}, [projectId, methodId, sampleId, invoke, subscribe, unsubscribe]);
|
|
398
|
+
|
|
354
399
|
// -----------------------------------------------------------------
|
|
355
400
|
// Raw-trace data fetch (lazy)
|
|
356
401
|
//
|
|
@@ -690,39 +735,53 @@ export const TestDataView: React.FC<TestDataViewProps> = (props) => {
|
|
|
690
735
|
</div>
|
|
691
736
|
</div>
|
|
692
737
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
738
|
+
{/* Size-to-content for small runs (≤ CYCLE_VIRTUAL_THRESHOLD
|
|
739
|
+
rows) so the Results panel below isn't pushed off-screen on
|
|
740
|
+
tests with one or two cycles. Larger runs flip to virtual
|
|
741
|
+
scrolling against `cycleTableHeight` so a 1000-cycle test
|
|
742
|
+
doesn't render 1000 row elements at once. */}
|
|
743
|
+
{(() => {
|
|
744
|
+
const useVirtual = cycles.length > CYCLE_VIRTUAL_THRESHOLD;
|
|
745
|
+
const cycleColumns: ScienceColumn[] = schema.cycle_fields.map(f => ({
|
|
746
|
+
field: f.name,
|
|
747
|
+
label: f.name,
|
|
748
|
+
units: f.units,
|
|
749
|
+
body: (row) => formatCell(row[f.name], f.type, f.scale),
|
|
750
|
+
}));
|
|
751
|
+
return (
|
|
752
|
+
<ScienceTable
|
|
753
|
+
title={`Cycle Data (${cycles.length})`}
|
|
754
|
+
columns={cycleColumns}
|
|
755
|
+
rows={cycles}
|
|
756
|
+
scrollable={useVirtual}
|
|
757
|
+
scrollHeight={useVirtual ? cycleTableHeight : undefined}
|
|
758
|
+
emptyMessage="No cycles yet."
|
|
759
|
+
/>
|
|
760
|
+
);
|
|
761
|
+
})()}
|
|
762
|
+
|
|
763
|
+
{/* Overlay the server-derived cross-test rollup (avg/min/max/
|
|
764
|
+
stddev/count) onto this run's own results. Aggregate fields
|
|
765
|
+
are never in `results` — the summary is their only source.
|
|
766
|
+
Rendered as a single-row report table (Bluehill style). */}
|
|
767
|
+
{(() => {
|
|
768
|
+
const values = { ...results, ...sampleSummary };
|
|
769
|
+
const hasValues = Object.keys(values).length > 0;
|
|
770
|
+
const resultColumns: ScienceColumn[] = schema.results_fields.map(f => ({
|
|
771
|
+
field: f.name,
|
|
772
|
+
label: f.name,
|
|
773
|
+
units: f.units,
|
|
774
|
+
body: (row) => formatCell(row[f.name], f.type, f.scale),
|
|
775
|
+
}));
|
|
776
|
+
return (
|
|
777
|
+
<ScienceTable
|
|
778
|
+
title="Results"
|
|
779
|
+
columns={resultColumns}
|
|
780
|
+
rows={hasValues ? [values] : []}
|
|
781
|
+
emptyMessage="No results yet."
|
|
782
|
+
/>
|
|
783
|
+
);
|
|
784
|
+
})()}
|
|
726
785
|
|
|
727
786
|
{/*
|
|
728
787
|
* View Raw Data dialog — tabular listing of the raw and
|
|
@@ -1088,24 +1147,6 @@ const formatNumeric = (v: any): string => {
|
|
|
1088
1147
|
return String(v);
|
|
1089
1148
|
};
|
|
1090
1149
|
|
|
1091
|
-
const ResultsGrid: React.FC<{ schema: TestFieldDef[]; values: any }> = ({ schema, values }) => {
|
|
1092
|
-
if (!values || Object.keys(values).length === 0) {
|
|
1093
|
-
return <div style={{ color: 'var(--text-secondary-color)' }}>No results yet.</div>;
|
|
1094
|
-
}
|
|
1095
|
-
return (
|
|
1096
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '0.5rem 1rem' }}>
|
|
1097
|
-
{schema.map(f => (
|
|
1098
|
-
<div key={f.name}>
|
|
1099
|
-
<div style={{ fontSize: '0.8em', color: 'var(--text-secondary-color)' }}>
|
|
1100
|
-
{f.name}{f.units ? ` (${f.units})` : ''}
|
|
1101
|
-
</div>
|
|
1102
|
-
<div>{formatCell(values[f.name], f.type, f.scale)}</div>
|
|
1103
|
-
</div>
|
|
1104
|
-
))}
|
|
1105
|
-
</div>
|
|
1106
|
-
);
|
|
1107
|
-
};
|
|
1108
|
-
|
|
1109
1150
|
/** Row count above which the Cycle Data table switches into virtual-
|
|
1110
1151
|
* scroll mode with a fixed `cycleTableHeight`. At or below the
|
|
1111
1152
|
* threshold the table sizes to its content so the Results panel
|
|
@@ -242,8 +242,12 @@ const rangeIssue = (f: TestFieldDef, raw: any): string | null => {
|
|
|
242
242
|
if (f.min == null && f.max == null) return null;
|
|
243
243
|
const disp = Number(rawToDisplay(Number(raw), f.scale));
|
|
244
244
|
if (!Number.isFinite(disp)) return null;
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
// Bounds form an unordered interval — an author may write the range in
|
|
246
|
+
// magnitude order (min:-50, max:-1000), which is numerically inverted.
|
|
247
|
+
const lo = f.min != null && f.max != null ? Math.min(f.min, f.max) : f.min;
|
|
248
|
+
const hi = f.min != null && f.max != null ? Math.max(f.min, f.max) : f.max;
|
|
249
|
+
if (lo != null && disp < lo) return `must be ≥ ${lo}`;
|
|
250
|
+
if (hi != null && disp > hi) return `must be ≤ ${hi}`;
|
|
247
251
|
return null;
|
|
248
252
|
};
|
|
249
253
|
|
|
@@ -462,6 +466,18 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
462
466
|
for (const field of schema.config_fields) {
|
|
463
467
|
if (field.name === 'sample_id') continue;
|
|
464
468
|
if (field.default === undefined || field.default === null) continue;
|
|
469
|
+
// A source-bound field's default must only SEED when GM has no
|
|
470
|
+
// value yet — never clobber an existing one. The defaults effect
|
|
471
|
+
// re-runs after every completed run (clearStagedConfig resets the
|
|
472
|
+
// marker), so without this guard the operator's setting (e.g.
|
|
473
|
+
// cof_window_pct) is reset to the literal default on each load.
|
|
474
|
+
// When GM already holds a value, keep it; the source-seed effect
|
|
475
|
+
// below mirrors the live value into stagedConfig.
|
|
476
|
+
if (field.source) {
|
|
477
|
+
const liveTag = findTagByFqdn(field.source);
|
|
478
|
+
const liveVal = liveTag ? rawValues[liveTag.tagName] : undefined;
|
|
479
|
+
if (liveVal !== undefined && liveVal !== null) continue;
|
|
480
|
+
}
|
|
465
481
|
// Resolve the default to a RAW value. Literals are authored in
|
|
466
482
|
// DISPLAY units (converted via `scale`); an `${fqdn}` token
|
|
467
483
|
// snapshots that tag's current (already-raw) value so a
|
|
@@ -17,7 +17,14 @@ const DESCRIPTIONS: Record<FieldArrayKey, string> = {
|
|
|
17
17
|
project_fields: 'System-level fields (operator, station). Filled by the HMI but not cycled.',
|
|
18
18
|
config_fields: 'Operator-input config (speeds, loads). Snapshotted into test.json on start.',
|
|
19
19
|
cycle_fields: 'Per-cycle capture. One row appended to cycles.jsonl per cycle.',
|
|
20
|
-
results_fields: 'Post-test summary (min/max/avg, pass/fail). Written once at finish.',
|
|
20
|
+
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
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Short human label for a field's aggregate spec, e.g. "avg(cof)" / "count". */
|
|
24
|
+
const aggregateLabel = (f: TestField): string => {
|
|
25
|
+
const a = f.aggregate;
|
|
26
|
+
if (!a) return '';
|
|
27
|
+
return a.fn === 'count' ? 'count' : `${a.fn}(${a.of ?? '?'})`;
|
|
21
28
|
};
|
|
22
29
|
|
|
23
30
|
export interface FieldArrayEditorProps {
|
|
@@ -116,6 +123,13 @@ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, me
|
|
|
116
123
|
style={{ width: '4rem' }}
|
|
117
124
|
/>
|
|
118
125
|
<Column field="label" header="Label" />
|
|
126
|
+
{arrayKey === 'results_fields' && (
|
|
127
|
+
<Column
|
|
128
|
+
header="Aggregate"
|
|
129
|
+
body={(r: TestField) => aggregateLabel(r)}
|
|
130
|
+
style={{ width: '8rem' }}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
119
133
|
<Column header="" body={rowActions} style={{ width: '10rem' }} />
|
|
120
134
|
</DataTable>
|
|
121
135
|
</FormSection>
|
|
@@ -123,6 +137,9 @@ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, me
|
|
|
123
137
|
visible={dialogOpen}
|
|
124
138
|
initial={editingIdx !== null ? (fields[editingIdx] ?? null) : null}
|
|
125
139
|
siblingNames={fields.map(f => f.name)}
|
|
140
|
+
isResultsField={arrayKey === 'results_fields'}
|
|
141
|
+
cycleFieldNames={(method.cycle_fields ?? []).map(f => f.name).filter(Boolean)}
|
|
142
|
+
resultsFieldNames={(method.results_fields ?? []).map(f => f.name).filter(Boolean)}
|
|
126
143
|
onCancel={() => setDialogOpen(false)}
|
|
127
144
|
onSave={handleSaveField}
|
|
128
145
|
/>
|
|
@@ -7,7 +7,7 @@ import { Dropdown } from 'primereact/dropdown';
|
|
|
7
7
|
import { Checkbox } from 'primereact/checkbox';
|
|
8
8
|
import { InputNumber } from 'primereact/inputnumber';
|
|
9
9
|
import { FormRow } from '../../forms/FormRow';
|
|
10
|
-
import type { TestField } from '../types';
|
|
10
|
+
import type { TestField, AggFn, AggFrom, AggregateSpec } from '../types';
|
|
11
11
|
|
|
12
12
|
const FIELD_TYPES: { label: string; value: string }[] = [
|
|
13
13
|
{ label: 'string', value: 'string' },
|
|
@@ -20,6 +20,21 @@ const FIELD_TYPES: { label: string; value: string }[] = [
|
|
|
20
20
|
{ label: 'bool', value: 'bool' },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
+
// '' = no aggregation (the field is supplied by the analysis script).
|
|
24
|
+
const AGG_FNS: { label: string; value: '' | AggFn }[] = [
|
|
25
|
+
{ label: '— none (analysis-fed) —', value: '' },
|
|
26
|
+
{ label: 'avg', value: 'avg' },
|
|
27
|
+
{ label: 'min', value: 'min' },
|
|
28
|
+
{ label: 'max', value: 'max' },
|
|
29
|
+
{ label: 'stddev', value: 'stddev' },
|
|
30
|
+
{ label: 'count (# included tests)', value: 'count' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const AGG_FROMS: { label: string; value: AggFrom }[] = [
|
|
34
|
+
{ label: 'cycle field (every cycle, every test)', value: 'cycle_fields' },
|
|
35
|
+
{ label: 'result (one value per test)', value: 'results' },
|
|
36
|
+
];
|
|
37
|
+
|
|
23
38
|
export interface TestFieldDialogProps {
|
|
24
39
|
visible: boolean;
|
|
25
40
|
initial: TestField | null;
|
|
@@ -27,12 +42,19 @@ export interface TestFieldDialogProps {
|
|
|
27
42
|
onSave: (field: TestField) => void;
|
|
28
43
|
/** Names already in use within the same array — for uniqueness validation. */
|
|
29
44
|
siblingNames: string[];
|
|
45
|
+
/** True when this field belongs to `results_fields` — only then is the
|
|
46
|
+
* cross-test Aggregate section shown. */
|
|
47
|
+
isResultsField?: boolean;
|
|
48
|
+
/** Candidate target field names for an aggregate's `of`, by source. */
|
|
49
|
+
cycleFieldNames?: string[];
|
|
50
|
+
resultsFieldNames?: string[];
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
const blank: TestField = { name: '', type: 'f32' };
|
|
33
54
|
|
|
34
55
|
export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
35
56
|
visible, initial, onCancel, onSave, siblingNames,
|
|
57
|
+
isResultsField = false, cycleFieldNames = [], resultsFieldNames = [],
|
|
36
58
|
}) => {
|
|
37
59
|
const [draft, setDraft] = useState<TestField>(blank);
|
|
38
60
|
// `default` is edited as free text so a numeric literal, a string, or an
|
|
@@ -73,9 +95,29 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
73
95
|
if (typeof f.min === 'number' && typeof f.max === 'number' && f.min > f.max) {
|
|
74
96
|
return `Min (${f.min}) cannot be greater than Max (${f.max}).`;
|
|
75
97
|
}
|
|
98
|
+
if (f.aggregate && f.aggregate.fn !== 'count' && !f.aggregate.of?.trim()) {
|
|
99
|
+
return `Aggregate "${f.aggregate.fn}" needs a target field (the field to ${f.aggregate.fn} across tests).`;
|
|
100
|
+
}
|
|
76
101
|
return null;
|
|
77
102
|
};
|
|
78
103
|
|
|
104
|
+
// Build the aggregate spec from the chosen function. '' clears it; count
|
|
105
|
+
// drops of/from (it reduces over the set of included tests, not a field).
|
|
106
|
+
const setAggFn = (fn: '' | AggFn) => {
|
|
107
|
+
if (fn === '') { setDraft({ ...draft, aggregate: undefined }); return; }
|
|
108
|
+
if (fn === 'count') { setDraft({ ...draft, aggregate: { fn: 'count' } }); return; }
|
|
109
|
+
const next: AggregateSpec = { fn, of: draft.aggregate?.of, from: draft.aggregate?.from ?? 'cycle_fields' };
|
|
110
|
+
setDraft({ ...draft, aggregate: next });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const agg = draft.aggregate;
|
|
114
|
+
const aggFrom: AggFrom = agg?.from ?? 'cycle_fields';
|
|
115
|
+
// `of` candidates depend on the source; exclude this field's own name so
|
|
116
|
+
// a result can't aggregate itself.
|
|
117
|
+
const aggTargets = (aggFrom === 'results' ? resultsFieldNames : cycleFieldNames)
|
|
118
|
+
.filter(n => n && n !== draft.name)
|
|
119
|
+
.map(n => ({ label: n, value: n }));
|
|
120
|
+
|
|
79
121
|
const handleSave = () => {
|
|
80
122
|
const candidate: TestField = { ...draft };
|
|
81
123
|
const dv = coerceDefault(defaultText);
|
|
@@ -173,6 +215,45 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
173
215
|
placeholder="e.g. 5 or ${gm.safe_speed}"
|
|
174
216
|
/>
|
|
175
217
|
</FormRow>
|
|
218
|
+
{isResultsField && (
|
|
219
|
+
<>
|
|
220
|
+
<div style={{ borderTop: '1px solid #e5e7eb', margin: '0.75rem 0 0.25rem' }} />
|
|
221
|
+
<FormRow
|
|
222
|
+
label="Aggregate"
|
|
223
|
+
hint="Cross-test rollup. When set, this result is derived server-side over the non-excluded tests sharing the same sample_id — the analysis script does not supply it."
|
|
224
|
+
>
|
|
225
|
+
<Dropdown
|
|
226
|
+
value={agg?.fn ?? ''}
|
|
227
|
+
options={AGG_FNS}
|
|
228
|
+
onChange={(e) => setAggFn(e.value)}
|
|
229
|
+
/>
|
|
230
|
+
</FormRow>
|
|
231
|
+
{agg && agg.fn !== 'count' && (
|
|
232
|
+
<>
|
|
233
|
+
<FormRow label="Aggregate from" hint="Which per-test source the target field is read from.">
|
|
234
|
+
<Dropdown
|
|
235
|
+
value={aggFrom}
|
|
236
|
+
options={AGG_FROMS}
|
|
237
|
+
onChange={(e) =>
|
|
238
|
+
setDraft({ ...draft, aggregate: { ...agg, from: e.value } })
|
|
239
|
+
}
|
|
240
|
+
/>
|
|
241
|
+
</FormRow>
|
|
242
|
+
<FormRow label="Aggregate of" required hint="The field to reduce across tests.">
|
|
243
|
+
<Dropdown
|
|
244
|
+
value={agg.of ?? null}
|
|
245
|
+
options={aggTargets}
|
|
246
|
+
onChange={(e) =>
|
|
247
|
+
setDraft({ ...draft, aggregate: { ...agg, of: e.value || undefined } })
|
|
248
|
+
}
|
|
249
|
+
editable
|
|
250
|
+
placeholder={aggTargets.length ? 'Select a field' : `No ${aggFrom} defined yet`}
|
|
251
|
+
/>
|
|
252
|
+
</FormRow>
|
|
253
|
+
</>
|
|
254
|
+
)}
|
|
255
|
+
</>
|
|
256
|
+
)}
|
|
176
257
|
<FormRow label="Description" hint="Hover tooltip in the form.">
|
|
177
258
|
<InputTextarea
|
|
178
259
|
rows={2}
|
|
@@ -27,6 +27,23 @@ export interface TestField {
|
|
|
27
27
|
* numeric input rejects values outside `[min, max]`. */
|
|
28
28
|
min?: number;
|
|
29
29
|
max?: number;
|
|
30
|
+
/** results_fields only: a cross-test rollup derived server-side over the
|
|
31
|
+
* non-excluded runs sharing this test's sample_id. When set, the field
|
|
32
|
+
* is NOT supplied by the analysis script — the server computes it. */
|
|
33
|
+
aggregate?: AggregateSpec;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type AggFn = 'avg' | 'min' | 'max' | 'stddev' | 'count';
|
|
37
|
+
export type AggFrom = 'cycle_fields' | 'results';
|
|
38
|
+
|
|
39
|
+
export interface AggregateSpec {
|
|
40
|
+
/** Reduction applied across the gathered values. */
|
|
41
|
+
fn: AggFn;
|
|
42
|
+
/** Field to aggregate — a cycle_field or results_field name (per `from`).
|
|
43
|
+
* Ignored for `fn: "count"` (which counts included tests). */
|
|
44
|
+
of?: string;
|
|
45
|
+
/** Which per-test source `of` is read from. Defaults to "cycle_fields". */
|
|
46
|
+
from?: AggFrom;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
export interface ChartAxis {
|
|
@@ -39,6 +39,41 @@ export function validateMethod(methodId: string, m: TestMethod): ValidationError
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Aggregate specs: only on results_fields; target must exist in the
|
|
43
|
+
// chosen source array and be numeric. `count` needs no target. Mirrors
|
|
44
|
+
// tis_servelet::validate_method.
|
|
45
|
+
const NUMERIC_TYPES = new Set(['i32', 'i64', 'u32', 'u64', 'f32', 'f64']);
|
|
46
|
+
const cycleTypes = new Map<string, string>(
|
|
47
|
+
((m.cycle_fields as TestField[] | undefined) ?? []).map((f) => [f.name, String(f.type)])
|
|
48
|
+
);
|
|
49
|
+
const resultTypes = new Map<string, string>(
|
|
50
|
+
((m.results_fields as TestField[] | undefined) ?? []).map((f) => [f.name, String(f.type)])
|
|
51
|
+
);
|
|
52
|
+
for (const key of ['project_fields', 'config_fields', 'cycle_fields'] as const) {
|
|
53
|
+
((m[key] as TestField[] | undefined) ?? []).forEach((f, i) => {
|
|
54
|
+
if (f.aggregate) {
|
|
55
|
+
errs.push({ path: `${key}.${i}.aggregate`, message: `${methodId}.${key}.${f.name}: aggregate is only valid on results_fields` });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
((m.results_fields as TestField[] | undefined) ?? []).forEach((f, i) => {
|
|
60
|
+
const a = f.aggregate;
|
|
61
|
+
if (!a || a.fn === 'count') return;
|
|
62
|
+
const of = a.of?.trim();
|
|
63
|
+
if (!of) {
|
|
64
|
+
errs.push({ path: `results_fields.${i}.aggregate`, message: `${methodId}.results_fields.${f.name}: aggregate "${a.fn}" requires a target field (of)` });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const lookup = (a.from ?? 'cycle_fields') === 'results' ? resultTypes : cycleTypes;
|
|
68
|
+
const srcLabel = (a.from ?? 'cycle_fields') === 'results' ? 'results_fields' : 'cycle_fields';
|
|
69
|
+
const ty = lookup.get(of);
|
|
70
|
+
if (ty === undefined) {
|
|
71
|
+
errs.push({ path: `results_fields.${i}.aggregate`, message: `${methodId}.results_fields.${f.name}: aggregate "of" references "${of}" which is not a ${srcLabel} of this method` });
|
|
72
|
+
} else if (!NUMERIC_TYPES.has(ty)) {
|
|
73
|
+
errs.push({ path: `results_fields.${i}.aggregate`, message: `${methodId}.results_fields.${f.name}: aggregate target "${of}" has non-numeric type "${ty}"` });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
42
77
|
// Build sets for axis-reference resolution.
|
|
43
78
|
const knownFields = new Set<string>();
|
|
44
79
|
for (const key of FIELD_ARRAY_KEYS) {
|
package/dist/assets/JogXNeg.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"JogXNeg.d.ts","sourceRoot":"","sources":["../../src/assets/JogXNeg.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,eAAO,MAAM,UAAU,GAAI,OAAO,QAAQ,CAAC,aAAa,CAAC,4CAkBxD,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
package/dist/assets/JogXNeg.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";export const SvgJogXNeg=o=>_jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",...o,children:[_jsx("path",{d:"M10 6 3 12 10 18Z",fill:"currentColor"}),_jsx("path",{d:"M14 6l8 12M22 6l-8 12"})]});export default SvgJogXNeg;
|
package/dist/assets/JogXPos.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"JogXPos.d.ts","sourceRoot":"","sources":["../../src/assets/JogXPos.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,eAAO,MAAM,UAAU,GAAI,OAAO,QAAQ,CAAC,aAAa,CAAC,4CAkBxD,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
package/dist/assets/JogXPos.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";export const SvgJogXPos=o=>_jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",...o,children:[_jsx("path",{d:"M2 6l8 12M10 6l-8 12"}),_jsx("path",{d:"M14 6 21 12 14 18Z",fill:"currentColor"})]});export default SvgJogXPos;
|