@adcops/autocore-react 3.3.84 → 3.3.87

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 (86) hide show
  1. package/dist/components/ValueInput.css +9 -12
  2. package/dist/components/ValueInput.d.ts +45 -154
  3. package/dist/components/ValueInput.d.ts.map +1 -1
  4. package/dist/components/ValueInput.js +1 -1
  5. package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
  6. package/dist/components/ams/AssetEditDialog.js +1 -1
  7. package/dist/components/forms/FormRow.d.ts +20 -0
  8. package/dist/components/forms/FormRow.d.ts.map +1 -0
  9. package/dist/components/forms/FormRow.js +1 -0
  10. package/dist/components/forms/FormSection.d.ts +19 -0
  11. package/dist/components/forms/FormSection.d.ts.map +1 -0
  12. package/dist/components/forms/FormSection.js +1 -0
  13. package/dist/components/forms/forms.css +89 -0
  14. package/dist/components/forms/index.d.ts +3 -0
  15. package/dist/components/forms/index.d.ts.map +1 -0
  16. package/dist/components/forms/index.js +1 -0
  17. package/dist/components/tis-editor/TisConfigEditor.css +121 -0
  18. package/dist/components/tis-editor/TisConfigEditor.d.ts +28 -0
  19. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -0
  20. package/dist/components/tis-editor/TisConfigEditor.js +1 -0
  21. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts +7 -0
  22. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts.map +1 -0
  23. package/dist/components/tis-editor/editor/AnalysisEditor.js +1 -0
  24. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts +10 -0
  25. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts.map +1 -0
  26. package/dist/components/tis-editor/editor/AssetRefsEditor.js +1 -0
  27. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +16 -0
  28. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -0
  29. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -0
  30. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts +8 -0
  31. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -0
  32. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -0
  33. package/dist/components/tis-editor/editor/IdentitySection.d.ts +7 -0
  34. package/dist/components/tis-editor/editor/IdentitySection.d.ts.map +1 -0
  35. package/dist/components/tis-editor/editor/IdentitySection.js +1 -0
  36. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts +20 -0
  37. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -0
  38. package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -0
  39. package/dist/components/tis-editor/editor/RawDataEditor.d.ts +7 -0
  40. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -0
  41. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -0
  42. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts +22 -0
  43. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -0
  44. package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -0
  45. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +11 -0
  46. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -0
  47. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -0
  48. package/dist/components/tis-editor/editor/ViewsEditor.d.ts +7 -0
  49. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -0
  50. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -0
  51. package/dist/components/tis-editor/types.d.ts +78 -0
  52. package/dist/components/tis-editor/types.d.ts.map +1 -0
  53. package/dist/components/tis-editor/types.js +1 -0
  54. package/dist/components/tis-editor/validation.d.ts +20 -0
  55. package/dist/components/tis-editor/validation.d.ts.map +1 -0
  56. package/dist/components/tis-editor/validation.js +1 -0
  57. package/dist/hooks/useAmsAssetTypes.d.ts +23 -0
  58. package/dist/hooks/useAmsAssetTypes.d.ts.map +1 -0
  59. package/dist/hooks/useAmsAssetTypes.js +1 -0
  60. package/dist/hooks/useTisConfig.d.ts +51 -0
  61. package/dist/hooks/useTisConfig.d.ts.map +1 -0
  62. package/dist/hooks/useTisConfig.js +1 -0
  63. package/package.json +9 -3
  64. package/src/components/ValueInput.css +9 -12
  65. package/src/components/ValueInput.tsx +132 -317
  66. package/src/components/ams/AssetEditDialog.tsx +357 -20
  67. package/src/components/forms/FormRow.tsx +37 -0
  68. package/src/components/forms/FormSection.tsx +39 -0
  69. package/src/components/forms/forms.css +89 -0
  70. package/src/components/forms/index.ts +2 -0
  71. package/src/components/tis-editor/TisConfigEditor.css +121 -0
  72. package/src/components/tis-editor/TisConfigEditor.tsx +321 -0
  73. package/src/components/tis-editor/editor/AnalysisEditor.tsx +54 -0
  74. package/src/components/tis-editor/editor/AssetRefsEditor.tsx +187 -0
  75. package/src/components/tis-editor/editor/ChartViewDialog.tsx +170 -0
  76. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +131 -0
  77. package/src/components/tis-editor/editor/IdentitySection.tsx +36 -0
  78. package/src/components/tis-editor/editor/MethodFormEditor.tsx +176 -0
  79. package/src/components/tis-editor/editor/RawDataEditor.tsx +117 -0
  80. package/src/components/tis-editor/editor/SaveDiffDialog.tsx +160 -0
  81. package/src/components/tis-editor/editor/TestFieldDialog.tsx +134 -0
  82. package/src/components/tis-editor/editor/ViewsEditor.tsx +101 -0
  83. package/src/components/tis-editor/types.ts +95 -0
  84. package/src/components/tis-editor/validation.ts +104 -0
  85. package/src/hooks/useAmsAssetTypes.ts +70 -0
  86. package/src/hooks/useTisConfig.ts +164 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * SaveDiffDialog — shows what's about to land on disk before save_config.
3
+ *
4
+ * Fetches a fresh disk-state via `tis.list_schemas` (the existing read
5
+ * endpoint that bypasses staging) and diffs against the current staged
6
+ * methods. Operator confirms or cancels.
7
+ *
8
+ * The diff is intentionally simple: added / removed / modified method IDs,
9
+ * plus a JSON before/after for each modified method. Adding a full
10
+ * line-level differ would mean another dep — not worth it for an HMI.
11
+ */
12
+
13
+ import { useEffect, useState } from 'react';
14
+ import { Dialog } from 'primereact/dialog';
15
+ import { Button } from 'primereact/button';
16
+ import type { TisIpcInvoker } from '../../../hooks/useTisConfig';
17
+ import type { TestMethod } from '../types';
18
+
19
+ export interface SaveDiffDialogProps {
20
+ visible: boolean;
21
+ staged: Record<string, TestMethod>;
22
+ invoker: TisIpcInvoker;
23
+ onConfirm: () => Promise<void> | void;
24
+ onCancel: () => void;
25
+ }
26
+
27
+ interface DiffSummary {
28
+ added: string[];
29
+ removed: string[];
30
+ modified: { id: string; before: TestMethod; after: TestMethod }[];
31
+ }
32
+
33
+ export const SaveDiffDialog: React.FC<SaveDiffDialogProps> = ({
34
+ visible, staged, invoker, onConfirm, onCancel,
35
+ }) => {
36
+ const [diff, setDiff] = useState<DiffSummary | null>(null);
37
+ const [error, setError] = useState<string | null>(null);
38
+ const [busy, setBusy] = useState<boolean>(false);
39
+
40
+ useEffect(() => {
41
+ if (!visible) return;
42
+ let cancelled = false;
43
+ (async () => {
44
+ setError(null);
45
+ try {
46
+ const resp = await invoker('tis.list_schemas', {});
47
+ if (cancelled) return;
48
+ if (!resp.success) {
49
+ setError(resp.error_message ?? 'tis.list_schemas failed');
50
+ return;
51
+ }
52
+ const disk = (resp.data?.test_methods ?? {}) as Record<string, TestMethod>;
53
+ setDiff(buildDiff(disk, staged));
54
+ } catch (e: any) {
55
+ setError(String(e?.message ?? e));
56
+ }
57
+ })();
58
+ return () => { cancelled = true; };
59
+ }, [visible, staged, invoker]);
60
+
61
+ const handleConfirm = async () => {
62
+ setBusy(true);
63
+ try {
64
+ await onConfirm();
65
+ } finally {
66
+ setBusy(false);
67
+ }
68
+ };
69
+
70
+ return (
71
+ <Dialog
72
+ header="Save Test Methods to Disk"
73
+ visible={visible}
74
+ onHide={onCancel}
75
+ style={{ width: '60rem', maxWidth: '90vw' }}
76
+ >
77
+ {error && <div style={{ color: '#dc2626', marginBottom: '0.5rem' }}>{error}</div>}
78
+ {!diff && !error && <small>Computing diff…</small>}
79
+ {diff && (
80
+ <div>
81
+ <p>
82
+ About to overwrite <code>project.json</code>. A backup
83
+ of the previous file will be written to{' '}
84
+ <code>project.json.bak</code>.
85
+ </p>
86
+ {diff.added.length === 0 && diff.removed.length === 0 && diff.modified.length === 0 && (
87
+ <p><em>No changes detected.</em></p>
88
+ )}
89
+ {diff.added.length > 0 && (
90
+ <p><strong>Added:</strong> {diff.added.join(', ')}</p>
91
+ )}
92
+ {diff.removed.length > 0 && (
93
+ <p><strong>Removed:</strong> {diff.removed.join(', ')}</p>
94
+ )}
95
+ {diff.modified.length > 0 && (
96
+ <>
97
+ <p><strong>Modified:</strong> {diff.modified.map(m => m.id).join(', ')}</p>
98
+ <details>
99
+ <summary>Show before / after</summary>
100
+ {diff.modified.map((m) => (
101
+ <div key={m.id} style={{ marginTop: '0.75rem' }}>
102
+ <strong>{m.id}</strong>
103
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.5rem', marginTop: '0.25rem' }}>
104
+ <pre style={preStyle('before')}>
105
+ {JSON.stringify(m.before, null, 2)}
106
+ </pre>
107
+ <pre style={preStyle('after')}>
108
+ {JSON.stringify(m.after, null, 2)}
109
+ </pre>
110
+ </div>
111
+ </div>
112
+ ))}
113
+ </details>
114
+ </>
115
+ )}
116
+ </div>
117
+ )}
118
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginTop: '1rem' }}>
119
+ <Button label="Cancel" className="p-button-text" onClick={onCancel} disabled={busy} />
120
+ <Button
121
+ label="Save"
122
+ icon="pi pi-save"
123
+ disabled={busy || !diff}
124
+ onClick={handleConfirm}
125
+ />
126
+ </div>
127
+ </Dialog>
128
+ );
129
+ };
130
+
131
+ function preStyle(kind: 'before' | 'after'): React.CSSProperties {
132
+ return {
133
+ background: kind === 'before' ? '#fef2f2' : '#f0fdf4',
134
+ color: '#0f172a',
135
+ padding: '0.5rem',
136
+ fontSize: '0.75rem',
137
+ margin: 0,
138
+ maxHeight: '20rem',
139
+ overflow: 'auto',
140
+ border: `1px solid ${kind === 'before' ? '#fecaca' : '#bbf7d0'}`,
141
+ borderRadius: 4,
142
+ };
143
+ }
144
+
145
+ function buildDiff(disk: Record<string, TestMethod>, staged: Record<string, TestMethod>): DiffSummary {
146
+ const out: DiffSummary = { added: [], removed: [], modified: [] };
147
+ const allKeys = new Set<string>([...Object.keys(disk), ...Object.keys(staged)]);
148
+ for (const k of allKeys) {
149
+ const d = disk[k];
150
+ const s = staged[k];
151
+ if (d === undefined && s !== undefined) out.added.push(k);
152
+ else if (d !== undefined && s === undefined) out.removed.push(k);
153
+ else if (d !== undefined && s !== undefined) {
154
+ if (JSON.stringify(d) !== JSON.stringify(s)) {
155
+ out.modified.push({ id: k, before: d, after: s });
156
+ }
157
+ }
158
+ }
159
+ return out;
160
+ }
@@ -0,0 +1,134 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Dialog } from 'primereact/dialog';
3
+ import { Button } from 'primereact/button';
4
+ import { InputText } from 'primereact/inputtext';
5
+ import { InputTextarea } from 'primereact/inputtextarea';
6
+ import { Dropdown } from 'primereact/dropdown';
7
+ import { Checkbox } from 'primereact/checkbox';
8
+ import { InputNumber } from 'primereact/inputnumber';
9
+ import { FormRow } from '../../forms/FormRow';
10
+ import type { TestField } from '../types';
11
+
12
+ const FIELD_TYPES: { label: string; value: string }[] = [
13
+ { label: 'string', value: 'string' },
14
+ { label: 'i32', value: 'i32' },
15
+ { label: 'i64', value: 'i64' },
16
+ { label: 'u32', value: 'u32' },
17
+ { label: 'u64', value: 'u64' },
18
+ { label: 'f32', value: 'f32' },
19
+ { label: 'f64', value: 'f64' },
20
+ { label: 'bool', value: 'bool' },
21
+ ];
22
+
23
+ export interface TestFieldDialogProps {
24
+ visible: boolean;
25
+ initial: TestField | null;
26
+ onCancel: () => void;
27
+ onSave: (field: TestField) => void;
28
+ /** Names already in use within the same array — for uniqueness validation. */
29
+ siblingNames: string[];
30
+ }
31
+
32
+ const blank: TestField = { name: '', type: 'f32' };
33
+
34
+ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
35
+ visible, initial, onCancel, onSave, siblingNames,
36
+ }) => {
37
+ const [draft, setDraft] = useState<TestField>(blank);
38
+ const [error, setError] = useState<string | null>(null);
39
+
40
+ useEffect(() => {
41
+ if (visible) {
42
+ setDraft(initial ? { ...initial } : { ...blank });
43
+ setError(null);
44
+ }
45
+ }, [visible, initial]);
46
+
47
+ const validate = (f: TestField): string | null => {
48
+ if (!f.name.trim()) return 'Field name is required.';
49
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(f.name)) {
50
+ return 'Name must be a valid identifier (letters, digits, underscore; cannot start with a digit).';
51
+ }
52
+ const dupOfOther = siblingNames.some(n => n === f.name && n !== initial?.name);
53
+ if (dupOfOther) return `A field named "${f.name}" already exists in this array.`;
54
+ return null;
55
+ };
56
+
57
+ const handleSave = () => {
58
+ const err = validate(draft);
59
+ if (err) { setError(err); return; }
60
+ onSave(draft);
61
+ };
62
+
63
+ return (
64
+ <Dialog
65
+ header={initial ? `Edit field: ${initial.name}` : 'New field'}
66
+ visible={visible}
67
+ onHide={onCancel}
68
+ style={{ width: '36rem' }}
69
+ >
70
+ {error && <div style={{ color: '#dc2626', marginBottom: '0.75rem' }}>{error}</div>}
71
+ <FormRow label="Name" required hint="Wire-format key. Also the column name in CSV exports.">
72
+ <InputText
73
+ value={draft.name}
74
+ onChange={(e) => setDraft({ ...draft, name: e.target.value })}
75
+ />
76
+ </FormRow>
77
+ <FormRow label="Type" required>
78
+ <Dropdown
79
+ value={draft.type}
80
+ options={FIELD_TYPES}
81
+ onChange={(e) => setDraft({ ...draft, type: e.value })}
82
+ editable
83
+ />
84
+ </FormRow>
85
+ <FormRow label="Units" hint="Display label, appended to form labels (e.g. m/s).">
86
+ <InputText
87
+ value={draft.units ?? ''}
88
+ onChange={(e) => setDraft({ ...draft, units: e.target.value || undefined })}
89
+ />
90
+ </FormRow>
91
+ <FormRow label="Label" hint="Pretty form label. Falls back to name when empty.">
92
+ <InputText
93
+ value={draft.label ?? ''}
94
+ onChange={(e) => setDraft({ ...draft, label: e.target.value || undefined })}
95
+ />
96
+ </FormRow>
97
+ <FormRow label="Required">
98
+ <Checkbox
99
+ checked={!!draft.required}
100
+ onChange={(e) => setDraft({ ...draft, required: !!e.checked })}
101
+ />
102
+ </FormRow>
103
+ <FormRow label="Source" hint="Optional gm.* variable to bind this field to.">
104
+ <InputText
105
+ value={draft.source ?? ''}
106
+ onChange={(e) => setDraft({ ...draft, source: e.target.value || undefined })}
107
+ placeholder="gm.<variable_name>"
108
+ />
109
+ </FormRow>
110
+ <FormRow label="Scale" hint="display = raw × scale. Leave blank for 1.0 (no conversion).">
111
+ <InputNumber
112
+ value={draft.scale ?? null}
113
+ onValueChange={(e) =>
114
+ setDraft({ ...draft, scale: typeof e.value === 'number' ? e.value : undefined })
115
+ }
116
+ mode="decimal"
117
+ minFractionDigits={0}
118
+ maxFractionDigits={9}
119
+ />
120
+ </FormRow>
121
+ <FormRow label="Description" hint="Hover tooltip in the form.">
122
+ <InputTextarea
123
+ rows={2}
124
+ value={draft.description ?? ''}
125
+ onChange={(e) => setDraft({ ...draft, description: e.target.value || undefined })}
126
+ />
127
+ </FormRow>
128
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginTop: '1rem' }}>
129
+ <Button label="Cancel" className="p-button-text" onClick={onCancel} />
130
+ <Button label="Save" onClick={handleSave} />
131
+ </div>
132
+ </Dialog>
133
+ );
134
+ };
@@ -0,0 +1,101 @@
1
+ import { useState } from 'react';
2
+ import { DataTable } from 'primereact/datatable';
3
+ import { Column } from 'primereact/column';
4
+ import { Button } from 'primereact/button';
5
+ import { FormSection } from '../../forms/FormSection';
6
+ import { ChartViewDialog } from './ChartViewDialog';
7
+ import type { ChartView, TestField, TestMethod } from '../types';
8
+
9
+ export interface ViewsEditorProps {
10
+ method: TestMethod;
11
+ onChange: (next: TestMethod) => void;
12
+ }
13
+
14
+ interface ViewRow {
15
+ id: string;
16
+ title: string;
17
+ type: string;
18
+ }
19
+
20
+ export const ViewsEditor: React.FC<ViewsEditorProps> = ({ method, onChange }) => {
21
+ const views = (method.views as Record<string, ChartView>) ?? {};
22
+ const rows: ViewRow[] = Object.entries(views).map(([id, v]) => ({
23
+ id, title: v.title ?? '', type: v.type ?? '',
24
+ }));
25
+
26
+ const [dialogOpen, setDialogOpen] = useState(false);
27
+ const [editingId, setEditingId] = useState<string | null>(null);
28
+
29
+ // Known field + column names get offered as datalist suggestions to the
30
+ // dialog, so chart axes pick from real values rather than typos.
31
+ const knownKeys: string[] = [
32
+ ...((method.project_fields ?? []) as TestField[]).map(f => f.name),
33
+ ...((method.config_fields ?? []) as TestField[]).map(f => f.name),
34
+ ...((method.cycle_fields ?? []) as TestField[]).map(f => f.name),
35
+ ...((method.results_fields ?? []) as TestField[]).map(f => f.name),
36
+ ...Object.keys((method.raw_data as any)?.columns ?? {}),
37
+ ];
38
+
39
+ const openNew = () => { setEditingId(null); setDialogOpen(true); };
40
+ const openEdit = (id: string) => { setEditingId(id); setDialogOpen(true); };
41
+
42
+ const handleSave = (newId: string, view: ChartView) => {
43
+ const next = { ...views };
44
+ // If editing an existing view and the user changed the id, drop the old key.
45
+ if (editingId && editingId !== newId) {
46
+ delete next[editingId];
47
+ }
48
+ next[newId] = view;
49
+ onChange({ ...method, views: next });
50
+ setDialogOpen(false);
51
+ };
52
+
53
+ const handleRemove = (id: string) => {
54
+ if (!window.confirm(`Remove view "${id}"?`)) return;
55
+ const next = { ...views };
56
+ delete next[id];
57
+ onChange({ ...method, views: next });
58
+ };
59
+
60
+ const rowActions = (r: ViewRow) => (
61
+ <div style={{ display: 'flex', gap: '0.25rem' }}>
62
+ <Button
63
+ icon="pi pi-pencil"
64
+ className="p-button-text p-button-sm"
65
+ onClick={() => openEdit(r.id)}
66
+ aria-label="Edit"
67
+ />
68
+ <Button
69
+ icon="pi pi-trash"
70
+ className="p-button-text p-button-danger p-button-sm"
71
+ onClick={() => handleRemove(r.id)}
72
+ aria-label="Remove"
73
+ />
74
+ </div>
75
+ );
76
+
77
+ return (
78
+ <>
79
+ <FormSection
80
+ title="Views"
81
+ description="Named chart definitions rendered by <TestDataView> (cycle_scatter) and <TestRawDataView> (raw_trace)."
82
+ actions={<Button label="Add view" icon="pi pi-plus" size="small" onClick={openNew} />}
83
+ >
84
+ <DataTable value={rows} dataKey="id" emptyMessage="No views defined.">
85
+ <Column field="id" header="View ID" />
86
+ <Column field="title" header="Title" />
87
+ <Column field="type" header="Type" style={{ width: '8rem' }} />
88
+ <Column header="" body={rowActions} style={{ width: '6rem' }} />
89
+ </DataTable>
90
+ </FormSection>
91
+ <ChartViewDialog
92
+ visible={dialogOpen}
93
+ initial={editingId ? { viewId: editingId, view: views[editingId] } : null}
94
+ knownKeys={knownKeys}
95
+ siblingIds={Object.keys(views)}
96
+ onCancel={() => setDialogOpen(false)}
97
+ onSave={handleSave}
98
+ />
99
+ </>
100
+ );
101
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Client-side mirror of the autocore-server TestMethod schema.
3
+ * Source of truth is autocore-server/src/project.rs.
4
+ *
5
+ * Kept loose — `unknown` for default values and pass-through fields — so
6
+ * unknown server-side extensions don't trip the editor at parse time.
7
+ */
8
+
9
+ export type FieldType =
10
+ | 'string'
11
+ | 'i32' | 'i64' | 'u32' | 'u64'
12
+ | 'f32' | 'f64'
13
+ | 'bool';
14
+
15
+ export interface TestField {
16
+ name: string;
17
+ type: FieldType | string;
18
+ units?: string;
19
+ required?: boolean;
20
+ source?: string;
21
+ label?: string;
22
+ description?: string;
23
+ default?: unknown;
24
+ scale?: number;
25
+ }
26
+
27
+ export interface ChartAxis {
28
+ field?: string;
29
+ column?: string;
30
+ label?: string;
31
+ }
32
+
33
+ export interface ChartSeries {
34
+ field?: string;
35
+ column?: string;
36
+ label?: string;
37
+ y_axis?: 'left' | 'right';
38
+ }
39
+
40
+ export type ChartViewType = 'cycle_scatter' | 'raw_trace';
41
+
42
+ export interface ChartView {
43
+ title?: string;
44
+ type: ChartViewType | string;
45
+ x: ChartAxis;
46
+ y: ChartSeries[];
47
+ }
48
+
49
+ export type RawColumnSource = 'time' | 'derived' | string; // also `ni.<daq>.channels.<name>`
50
+
51
+ export interface RawColumn {
52
+ source: RawColumnSource;
53
+ formula?: string;
54
+ }
55
+
56
+ export interface RawDataShape {
57
+ blob_name: string;
58
+ columns: Record<string, RawColumn>;
59
+ units?: Record<string, string>;
60
+ }
61
+
62
+ export type AssetRefSelect = 'by_location' | 'by_id_field';
63
+ export type CalibrationPolicy = 'ignore' | 'warn' | 'require';
64
+
65
+ export interface AssetRef {
66
+ field: string;
67
+ asset_type: string;
68
+ select: AssetRefSelect | string;
69
+ location?: string;
70
+ from?: string;
71
+ calibration_required?: CalibrationPolicy | string;
72
+ label?: string;
73
+ description?: string;
74
+ }
75
+
76
+ export interface AnalysisShape {
77
+ script: string;
78
+ function: string;
79
+ }
80
+
81
+ export interface TestMethod {
82
+ label?: string;
83
+ description?: string;
84
+ project_fields?: TestField[];
85
+ config_fields?: TestField[];
86
+ cycle_fields?: TestField[];
87
+ results_fields?: TestField[];
88
+ raw_data?: RawDataShape | null;
89
+ views?: Record<string, ChartView>;
90
+ asset_refs?: AssetRef[];
91
+ analysis?: AnalysisShape | null;
92
+ [key: string]: unknown; // tolerate unknown server-side fields
93
+ }
94
+
95
+ export type FieldArrayKey = 'project_fields' | 'config_fields' | 'cycle_fields' | 'results_fields';
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Client-side validation for TestMethod. Mirrors the cross-field checks
3
+ * in autocore-server/src/tis_servelet.rs `validate_method` so the operator
4
+ * sees the same errors instantly in the form, not only after Apply.
5
+ *
6
+ * Returns a flat array of error messages. Empty array = clean.
7
+ */
8
+
9
+ import type {
10
+ TestField, TestMethod, ChartAxis, ChartSeries,
11
+ } from './types';
12
+
13
+ const FIELD_ARRAY_KEYS = ['project_fields', 'config_fields', 'cycle_fields', 'results_fields'] as const;
14
+
15
+ export interface ValidationError {
16
+ /** Dotted path: "<arrayKey>.<index>.<sub>" or "views.<viewId>.x" etc. */
17
+ path: string;
18
+ message: string;
19
+ }
20
+
21
+ export function validateMethod(methodId: string, m: TestMethod): ValidationError[] {
22
+ const errs: ValidationError[] = [];
23
+
24
+ // Field-name uniqueness + non-empty within each array.
25
+ for (const key of FIELD_ARRAY_KEYS) {
26
+ const arr = (m[key] as TestField[] | undefined) ?? [];
27
+ const seen = new Set<string>();
28
+ arr.forEach((f, i) => {
29
+ if (!f.name || f.name.trim() === '') {
30
+ errs.push({ path: `${key}.${i}.name`, message: `${methodId}.${key}: empty field name` });
31
+ } else if (seen.has(f.name)) {
32
+ errs.push({ path: `${key}.${i}.name`, message: `${methodId}.${key}: duplicate field name "${f.name}"` });
33
+ } else {
34
+ seen.add(f.name);
35
+ }
36
+ });
37
+ }
38
+
39
+ // Build sets for axis-reference resolution.
40
+ const knownFields = new Set<string>();
41
+ for (const key of FIELD_ARRAY_KEYS) {
42
+ const arr = (m[key] as TestField[] | undefined) ?? [];
43
+ arr.forEach((f) => f.name && knownFields.add(f.name));
44
+ }
45
+ const knownColumns = new Set<string>(
46
+ Object.keys(((m.raw_data as any)?.columns ?? {}) as Record<string, unknown>)
47
+ );
48
+
49
+ const resolveRef = (a: ChartAxis | ChartSeries): { key: string; ok: boolean } | null => {
50
+ if (a.field && a.field.trim()) {
51
+ return { key: a.field, ok: knownFields.has(a.field) };
52
+ }
53
+ if (a.column && a.column.trim()) {
54
+ return { key: a.column, ok: knownColumns.has(a.column) };
55
+ }
56
+ return null;
57
+ };
58
+
59
+ const views = (m.views as Record<string, any>) ?? {};
60
+ for (const [viewId, view] of Object.entries(views)) {
61
+ if (view?.x) {
62
+ const ref = resolveRef(view.x);
63
+ if (ref && !ref.ok) {
64
+ errs.push({
65
+ path: `views.${viewId}.x`,
66
+ message: `${methodId}.views.${viewId}: x.field/column "${ref.key}" does not match any field or raw_data column`,
67
+ });
68
+ }
69
+ }
70
+ const ys: ChartSeries[] = view?.y ?? [];
71
+ ys.forEach((s, i) => {
72
+ const ref = resolveRef(s);
73
+ if (ref && !ref.ok) {
74
+ errs.push({
75
+ path: `views.${viewId}.y.${i}`,
76
+ message: `${methodId}.views.${viewId}.y[${i}]: field/column "${ref.key}" does not match any field or raw_data column`,
77
+ });
78
+ }
79
+ });
80
+ }
81
+
82
+ // Raw-data blob_name non-empty.
83
+ const rd = m.raw_data as any;
84
+ if (rd) {
85
+ if (!rd.blob_name || String(rd.blob_name).trim() === '') {
86
+ errs.push({ path: 'raw_data.blob_name', message: `${methodId}.raw_data: blob_name is empty` });
87
+ }
88
+ }
89
+
90
+ return errs;
91
+ }
92
+
93
+ /**
94
+ * Validate a whole methods map. Returns errors grouped by methodId.
95
+ * Useful for the master-detail sidebar (display an error count per row).
96
+ */
97
+ export function validateMethods(methods: Record<string, TestMethod>): Record<string, ValidationError[]> {
98
+ const out: Record<string, ValidationError[]> = {};
99
+ for (const [id, m] of Object.entries(methods)) {
100
+ const errs = validateMethod(id, m);
101
+ if (errs.length > 0) out[id] = errs;
102
+ }
103
+ return out;
104
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * useAmsAssetTypes — fetch the AMS asset-type catalog.
3
+ *
4
+ * Used by the TIS editor's AssetRefsEditor to populate the asset_type
5
+ * dropdown with real values instead of free-form text. Falls back to
6
+ * an empty array on error (the dropdown degrades gracefully to a text
7
+ * field).
8
+ *
9
+ * Wraps `ams.list_schemas`, which returns the merged catalog of
10
+ * built-in + project-declared asset types. Accepts an `invoker` override
11
+ * for the playground / tests.
12
+ */
13
+
14
+ import { useCallback, useContext, useEffect, useState } from 'react';
15
+ import { EventEmitterContext } from '../core/EventEmitterContext';
16
+ import { MessageType } from '../hub/CommandMessage';
17
+ import type { TisIpcInvoker } from './useTisConfig';
18
+
19
+ export interface UseAmsAssetTypesResult {
20
+ types: string[];
21
+ loading: boolean;
22
+ error: string | null;
23
+ refresh: () => Promise<void>;
24
+ }
25
+
26
+ export function useAmsAssetTypes(options?: { invoker?: TisIpcInvoker }): UseAmsAssetTypesResult {
27
+ const ctx = useContext(EventEmitterContext);
28
+ const invoker: TisIpcInvoker = options?.invoker
29
+ ?? (async (topic, payload) => {
30
+ return await ctx.invoke(topic as any, MessageType.Request, payload as any);
31
+ });
32
+
33
+ const [types, setTypes] = useState<string[]>([]);
34
+ const [loading, setLoading] = useState<boolean>(true);
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ const refresh = useCallback(async () => {
38
+ setLoading(true);
39
+ try {
40
+ const resp = await invoker('ams.list_schemas', {});
41
+ if (!resp.success) {
42
+ setError(resp.error_message ?? 'ams.list_schemas failed');
43
+ setTypes([]);
44
+ return;
45
+ }
46
+ // The wire shape carries asset types as a map keyed by type name.
47
+ // Be lenient about exact field names so we degrade gracefully if
48
+ // the server response evolves.
49
+ const d = resp.data ?? {};
50
+ const catalog =
51
+ (d.asset_types && typeof d.asset_types === 'object') ? d.asset_types :
52
+ (d.schemas && typeof d.schemas === 'object') ? d.schemas :
53
+ null;
54
+ setTypes(catalog ? Object.keys(catalog) : []);
55
+ setError(null);
56
+ } catch (e: any) {
57
+ setError(String(e?.message ?? e));
58
+ setTypes([]);
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, [invoker]);
64
+
65
+ useEffect(() => {
66
+ void refresh();
67
+ }, [refresh]);
68
+
69
+ return { types, loading, error, refresh };
70
+ }