@adcops/autocore-react 3.3.105 → 3.3.109
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 +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.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/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +5 -0
- 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 +20 -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/components/index.ts +3 -0
- package/src/components/tis/ResultHistoryTable.css +12 -0
- package/src/components/tis/ResultHistoryTable.tsx +61 -0
- package/src/components/tis/SampleSummaryPanel.tsx +171 -0
- package/src/components/tis/TestDataView.tsx +47 -1
- package/src/components/tis/TestSetupForm.tsx +104 -15
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +18 -1
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +139 -3
- package/src/components/tis-editor/types.ts +22 -0
- package/src/components/tis-editor/validation.ts +38 -0
|
@@ -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,23 +42,49 @@ 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);
|
|
60
|
+
// `default` is edited as free text so a numeric literal, a string, or an
|
|
61
|
+
// `${fqdn}` token can all be entered in one field; it's coerced on save.
|
|
62
|
+
const [defaultText, setDefaultText] = useState<string>('');
|
|
38
63
|
const [error, setError] = useState<string | null>(null);
|
|
39
64
|
|
|
40
65
|
useEffect(() => {
|
|
41
66
|
if (visible) {
|
|
42
67
|
setDraft(initial ? { ...initial } : { ...blank });
|
|
68
|
+
setDefaultText(initial?.default == null ? '' : String(initial.default));
|
|
43
69
|
setError(null);
|
|
44
70
|
}
|
|
45
71
|
}, [visible, initial]);
|
|
46
72
|
|
|
73
|
+
// Turn the free-text Default into its stored form: an `${fqdn}` token and
|
|
74
|
+
// other non-numerics stay strings; numeric/bool literals are stored typed.
|
|
75
|
+
const coerceDefault = (s: string): unknown => {
|
|
76
|
+
const t = s.trim();
|
|
77
|
+
if (t === '') return undefined;
|
|
78
|
+
if (t.startsWith('${')) return t;
|
|
79
|
+
if (t === 'true') return true;
|
|
80
|
+
if (t === 'false') return false;
|
|
81
|
+
if (/^-?\d*\.?\d+$/.test(t)) {
|
|
82
|
+
const n = Number(t);
|
|
83
|
+
if (Number.isFinite(n)) return n;
|
|
84
|
+
}
|
|
85
|
+
return t;
|
|
86
|
+
};
|
|
87
|
+
|
|
47
88
|
const validate = (f: TestField): string | null => {
|
|
48
89
|
if (!f.name.trim()) return 'Field name is required.';
|
|
49
90
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(f.name)) {
|
|
@@ -51,13 +92,40 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
51
92
|
}
|
|
52
93
|
const dupOfOther = siblingNames.some(n => n === f.name && n !== initial?.name);
|
|
53
94
|
if (dupOfOther) return `A field named "${f.name}" already exists in this array.`;
|
|
95
|
+
if (typeof f.min === 'number' && typeof f.max === 'number' && f.min > f.max) {
|
|
96
|
+
return `Min (${f.min}) cannot be greater than Max (${f.max}).`;
|
|
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
|
+
}
|
|
54
101
|
return null;
|
|
55
102
|
};
|
|
56
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
|
+
|
|
57
121
|
const handleSave = () => {
|
|
58
|
-
const
|
|
122
|
+
const candidate: TestField = { ...draft };
|
|
123
|
+
const dv = coerceDefault(defaultText);
|
|
124
|
+
if (dv === undefined) delete (candidate as { default?: unknown }).default;
|
|
125
|
+
else candidate.default = dv;
|
|
126
|
+
const err = validate(candidate);
|
|
59
127
|
if (err) { setError(err); return; }
|
|
60
|
-
onSave(
|
|
128
|
+
onSave(candidate);
|
|
61
129
|
};
|
|
62
130
|
|
|
63
131
|
return (
|
|
@@ -118,6 +186,74 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
118
186
|
maxFractionDigits={9}
|
|
119
187
|
/>
|
|
120
188
|
</FormRow>
|
|
189
|
+
<FormRow label="Min" hint="Optional lower bound the operator can enter (display units). Blank = no minimum.">
|
|
190
|
+
<InputNumber
|
|
191
|
+
value={draft.min ?? null}
|
|
192
|
+
onValueChange={(e) =>
|
|
193
|
+
setDraft({ ...draft, min: typeof e.value === 'number' ? e.value : undefined })
|
|
194
|
+
}
|
|
195
|
+
mode="decimal"
|
|
196
|
+
minFractionDigits={0}
|
|
197
|
+
maxFractionDigits={9}
|
|
198
|
+
/>
|
|
199
|
+
</FormRow>
|
|
200
|
+
<FormRow label="Max" hint="Optional upper bound the operator can enter (display units). Blank = no maximum.">
|
|
201
|
+
<InputNumber
|
|
202
|
+
value={draft.max ?? null}
|
|
203
|
+
onValueChange={(e) =>
|
|
204
|
+
setDraft({ ...draft, max: typeof e.value === 'number' ? e.value : undefined })
|
|
205
|
+
}
|
|
206
|
+
mode="decimal"
|
|
207
|
+
minFractionDigits={0}
|
|
208
|
+
maxFractionDigits={9}
|
|
209
|
+
/>
|
|
210
|
+
</FormRow>
|
|
211
|
+
<FormRow label="Default" hint="Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.">
|
|
212
|
+
<InputText
|
|
213
|
+
value={defaultText}
|
|
214
|
+
onChange={(e) => setDefaultText(e.target.value)}
|
|
215
|
+
placeholder="e.g. 5 or ${gm.safe_speed}"
|
|
216
|
+
/>
|
|
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
|
+
)}
|
|
121
257
|
<FormRow label="Description" hint="Hover tooltip in the form.">
|
|
122
258
|
<InputTextarea
|
|
123
259
|
rows={2}
|
|
@@ -22,6 +22,28 @@ export interface TestField {
|
|
|
22
22
|
description?: string;
|
|
23
23
|
default?: unknown;
|
|
24
24
|
scale?: number;
|
|
25
|
+
/** Optional inclusive bounds for the operator's numeric value, authored
|
|
26
|
+
* in display units (same convention as `default`/`scale`). The form's
|
|
27
|
+
* numeric input rejects values outside `[min, max]`. */
|
|
28
|
+
min?: number;
|
|
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;
|
|
25
47
|
}
|
|
26
48
|
|
|
27
49
|
export interface ChartAxis {
|
|
@@ -33,8 +33,46 @@ export function validateMethod(methodId: string, m: TestMethod): ValidationError
|
|
|
33
33
|
} else {
|
|
34
34
|
seen.add(f.name);
|
|
35
35
|
}
|
|
36
|
+
if (typeof f.min === 'number' && typeof f.max === 'number' && f.min > f.max) {
|
|
37
|
+
errs.push({ path: `${key}.${i}.min`, message: `${methodId}.${key}.${f.name}: min (${f.min}) is greater than max (${f.max})` });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
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
|
+
}
|
|
36
57
|
});
|
|
37
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
|
+
});
|
|
38
76
|
|
|
39
77
|
// Build sets for axis-reference resolution.
|
|
40
78
|
const knownFields = new Set<string>();
|