@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.
Files changed (35) hide show
  1. package/dist/components/index.d.ts +2 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/components/tis/ResultHistoryTable.css +12 -0
  5. package/dist/components/tis/ResultHistoryTable.d.ts +1 -0
  6. package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
  7. package/dist/components/tis/ResultHistoryTable.js +1 -1
  8. package/dist/components/tis/SampleSummaryPanel.d.ts +23 -0
  9. package/dist/components/tis/SampleSummaryPanel.d.ts.map +1 -0
  10. package/dist/components/tis/SampleSummaryPanel.js +1 -0
  11. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  12. package/dist/components/tis/TestDataView.js +1 -1
  13. package/dist/components/tis/TestSetupForm.d.ts +5 -0
  14. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  15. package/dist/components/tis/TestSetupForm.js +1 -1
  16. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
  17. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
  18. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +6 -0
  19. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  20. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  21. package/dist/components/tis-editor/types.d.ts +20 -0
  22. package/dist/components/tis-editor/types.d.ts.map +1 -1
  23. package/dist/components/tis-editor/validation.d.ts.map +1 -1
  24. package/dist/components/tis-editor/validation.js +1 -1
  25. package/package.json +1 -1
  26. package/src/components/index.ts +3 -0
  27. package/src/components/tis/ResultHistoryTable.css +12 -0
  28. package/src/components/tis/ResultHistoryTable.tsx +61 -0
  29. package/src/components/tis/SampleSummaryPanel.tsx +171 -0
  30. package/src/components/tis/TestDataView.tsx +47 -1
  31. package/src/components/tis/TestSetupForm.tsx +104 -15
  32. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +18 -1
  33. package/src/components/tis-editor/editor/TestFieldDialog.tsx +139 -3
  34. package/src/components/tis-editor/types.ts +22 -0
  35. 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 err = validate(draft);
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(draft);
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>();