@adcops/autocore-react 3.3.85 → 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 (83) 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/forms/FormRow.d.ts +20 -0
  6. package/dist/components/forms/FormRow.d.ts.map +1 -0
  7. package/dist/components/forms/FormRow.js +1 -0
  8. package/dist/components/forms/FormSection.d.ts +19 -0
  9. package/dist/components/forms/FormSection.d.ts.map +1 -0
  10. package/dist/components/forms/FormSection.js +1 -0
  11. package/dist/components/forms/forms.css +89 -0
  12. package/dist/components/forms/index.d.ts +3 -0
  13. package/dist/components/forms/index.d.ts.map +1 -0
  14. package/dist/components/forms/index.js +1 -0
  15. package/dist/components/tis-editor/TisConfigEditor.css +121 -0
  16. package/dist/components/tis-editor/TisConfigEditor.d.ts +28 -0
  17. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -0
  18. package/dist/components/tis-editor/TisConfigEditor.js +1 -0
  19. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts +7 -0
  20. package/dist/components/tis-editor/editor/AnalysisEditor.d.ts.map +1 -0
  21. package/dist/components/tis-editor/editor/AnalysisEditor.js +1 -0
  22. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts +10 -0
  23. package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts.map +1 -0
  24. package/dist/components/tis-editor/editor/AssetRefsEditor.js +1 -0
  25. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +16 -0
  26. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -0
  27. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -0
  28. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts +8 -0
  29. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -0
  30. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -0
  31. package/dist/components/tis-editor/editor/IdentitySection.d.ts +7 -0
  32. package/dist/components/tis-editor/editor/IdentitySection.d.ts.map +1 -0
  33. package/dist/components/tis-editor/editor/IdentitySection.js +1 -0
  34. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts +20 -0
  35. package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -0
  36. package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -0
  37. package/dist/components/tis-editor/editor/RawDataEditor.d.ts +7 -0
  38. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -0
  39. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -0
  40. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts +22 -0
  41. package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -0
  42. package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -0
  43. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +11 -0
  44. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -0
  45. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -0
  46. package/dist/components/tis-editor/editor/ViewsEditor.d.ts +7 -0
  47. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -0
  48. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -0
  49. package/dist/components/tis-editor/types.d.ts +78 -0
  50. package/dist/components/tis-editor/types.d.ts.map +1 -0
  51. package/dist/components/tis-editor/types.js +1 -0
  52. package/dist/components/tis-editor/validation.d.ts +20 -0
  53. package/dist/components/tis-editor/validation.d.ts.map +1 -0
  54. package/dist/components/tis-editor/validation.js +1 -0
  55. package/dist/hooks/useAmsAssetTypes.d.ts +23 -0
  56. package/dist/hooks/useAmsAssetTypes.d.ts.map +1 -0
  57. package/dist/hooks/useAmsAssetTypes.js +1 -0
  58. package/dist/hooks/useTisConfig.d.ts +51 -0
  59. package/dist/hooks/useTisConfig.d.ts.map +1 -0
  60. package/dist/hooks/useTisConfig.js +1 -0
  61. package/package.json +9 -3
  62. package/src/components/ValueInput.css +9 -12
  63. package/src/components/ValueInput.tsx +132 -317
  64. package/src/components/forms/FormRow.tsx +37 -0
  65. package/src/components/forms/FormSection.tsx +39 -0
  66. package/src/components/forms/forms.css +89 -0
  67. package/src/components/forms/index.ts +2 -0
  68. package/src/components/tis-editor/TisConfigEditor.css +121 -0
  69. package/src/components/tis-editor/TisConfigEditor.tsx +321 -0
  70. package/src/components/tis-editor/editor/AnalysisEditor.tsx +54 -0
  71. package/src/components/tis-editor/editor/AssetRefsEditor.tsx +187 -0
  72. package/src/components/tis-editor/editor/ChartViewDialog.tsx +170 -0
  73. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +131 -0
  74. package/src/components/tis-editor/editor/IdentitySection.tsx +36 -0
  75. package/src/components/tis-editor/editor/MethodFormEditor.tsx +176 -0
  76. package/src/components/tis-editor/editor/RawDataEditor.tsx +117 -0
  77. package/src/components/tis-editor/editor/SaveDiffDialog.tsx +160 -0
  78. package/src/components/tis-editor/editor/TestFieldDialog.tsx +134 -0
  79. package/src/components/tis-editor/editor/ViewsEditor.tsx +101 -0
  80. package/src/components/tis-editor/types.ts +95 -0
  81. package/src/components/tis-editor/validation.ts +104 -0
  82. package/src/hooks/useAmsAssetTypes.ts +70 -0
  83. package/src/hooks/useTisConfig.ts +164 -0
@@ -0,0 +1,170 @@
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 { Dropdown } from 'primereact/dropdown';
6
+ import { FormRow } from '../../forms/FormRow';
7
+ import type { ChartAxis, ChartSeries, ChartView } from '../types';
8
+
9
+ const VIEW_TYPES = [
10
+ { label: 'Cycle scatter', value: 'cycle_scatter' },
11
+ { label: 'Raw trace', value: 'raw_trace' },
12
+ ];
13
+
14
+ const Y_AXES = [
15
+ { label: 'Left', value: 'left' },
16
+ { label: 'Right', value: 'right' },
17
+ ];
18
+
19
+ export interface ChartViewDialogProps {
20
+ visible: boolean;
21
+ initial: { viewId: string; view: ChartView } | null;
22
+ onCancel: () => void;
23
+ onSave: (viewId: string, view: ChartView) => void;
24
+ /** Known field/column names; offered as datalist suggestions. */
25
+ knownKeys: string[];
26
+ /** Other view ids already in use — for uniqueness validation. */
27
+ siblingIds: string[];
28
+ }
29
+
30
+ const blank = (): ChartView => ({
31
+ title: '', type: 'cycle_scatter',
32
+ x: { field: '', label: '' },
33
+ y: [],
34
+ });
35
+
36
+ export const ChartViewDialog: React.FC<ChartViewDialogProps> = ({
37
+ visible, initial, onCancel, onSave, knownKeys, siblingIds,
38
+ }) => {
39
+ const [viewId, setViewId] = useState<string>('');
40
+ const [draft, setDraft] = useState<ChartView>(blank());
41
+ const [error, setError] = useState<string | null>(null);
42
+
43
+ useEffect(() => {
44
+ if (visible) {
45
+ setViewId(initial?.viewId ?? '');
46
+ setDraft(initial ? JSON.parse(JSON.stringify(initial.view)) : blank());
47
+ setError(null);
48
+ }
49
+ }, [visible, initial]);
50
+
51
+ const validate = (): string | null => {
52
+ if (!viewId.trim()) return 'View ID is required.';
53
+ if (siblingIds.includes(viewId) && viewId !== initial?.viewId) {
54
+ return `A view named "${viewId}" already exists.`;
55
+ }
56
+ return null;
57
+ };
58
+
59
+ const handleSave = () => {
60
+ const err = validate();
61
+ if (err) { setError(err); return; }
62
+ onSave(viewId, draft);
63
+ };
64
+
65
+ // Choose which axis property to bind to based on view type.
66
+ const isRawTrace = draft.type === 'raw_trace';
67
+ const axisKey: 'column' | 'field' = isRawTrace ? 'column' : 'field';
68
+
69
+ const setAxis = (patch: Partial<ChartAxis>) => {
70
+ setDraft({ ...draft, x: { ...draft.x, ...patch } });
71
+ };
72
+ const setSeries = (i: number, patch: Partial<ChartSeries>) => {
73
+ const next = [...draft.y];
74
+ next[i] = { ...next[i], ...patch };
75
+ setDraft({ ...draft, y: next });
76
+ };
77
+ const addSeries = () => {
78
+ setDraft({ ...draft, y: [...draft.y, { [axisKey]: '', label: '', y_axis: 'left' } as ChartSeries] });
79
+ };
80
+ const removeSeries = (i: number) => {
81
+ setDraft({ ...draft, y: draft.y.filter((_, idx) => idx !== i) });
82
+ };
83
+
84
+ return (
85
+ <Dialog
86
+ header={initial ? `Edit view: ${initial.viewId}` : 'New chart view'}
87
+ visible={visible}
88
+ onHide={onCancel}
89
+ style={{ width: '42rem' }}
90
+ >
91
+ {error && <div style={{ color: '#dc2626', marginBottom: '0.5rem' }}>{error}</div>}
92
+ <FormRow label="View ID" required hint="Stable key (e.g. cof_scatter). Cannot collide with other views.">
93
+ <InputText value={viewId} onChange={(e) => setViewId(e.target.value)} />
94
+ </FormRow>
95
+ <FormRow label="Title">
96
+ <InputText value={draft.title ?? ''} onChange={(e) => setDraft({ ...draft, title: e.target.value })} />
97
+ </FormRow>
98
+ <FormRow label="Type" required>
99
+ <Dropdown
100
+ value={draft.type}
101
+ options={VIEW_TYPES}
102
+ onChange={(e) => setDraft({ ...draft, type: e.value })}
103
+ />
104
+ </FormRow>
105
+ <FormRow label={isRawTrace ? 'X column' : 'X field'} required>
106
+ <InputText
107
+ value={(draft.x[axisKey] as string) ?? ''}
108
+ onChange={(e) => setAxis({ [axisKey]: e.target.value } as ChartAxis)}
109
+ list="known-keys"
110
+ />
111
+ </FormRow>
112
+ <FormRow label="X label">
113
+ <InputText
114
+ value={draft.x.label ?? ''}
115
+ onChange={(e) => setAxis({ label: e.target.value })}
116
+ />
117
+ </FormRow>
118
+
119
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', margin: '1rem 0 0.5rem' }}>
120
+ <strong>Y series</strong>
121
+ <Button label="Add series" icon="pi pi-plus" size="small" onClick={addSeries} />
122
+ </div>
123
+ {draft.y.length === 0 && <small>No series — add at least one.</small>}
124
+ {draft.y.map((s, i) => (
125
+ <div
126
+ key={i}
127
+ style={{
128
+ display: 'grid',
129
+ gridTemplateColumns: '1fr 1fr 6rem 2rem',
130
+ gap: '0.5rem',
131
+ marginBottom: '0.25rem',
132
+ alignItems: 'center',
133
+ }}
134
+ >
135
+ <InputText
136
+ placeholder={isRawTrace ? 'column' : 'field'}
137
+ value={(s[axisKey] as string) ?? ''}
138
+ onChange={(e) => setSeries(i, { [axisKey]: e.target.value } as ChartSeries)}
139
+ list="known-keys"
140
+ />
141
+ <InputText
142
+ placeholder="label"
143
+ value={s.label ?? ''}
144
+ onChange={(e) => setSeries(i, { label: e.target.value })}
145
+ />
146
+ <Dropdown
147
+ value={s.y_axis ?? 'left'}
148
+ options={Y_AXES}
149
+ onChange={(e) => setSeries(i, { y_axis: e.value })}
150
+ />
151
+ <Button
152
+ icon="pi pi-trash"
153
+ className="p-button-text p-button-danger p-button-sm"
154
+ onClick={() => removeSeries(i)}
155
+ aria-label="Remove series"
156
+ />
157
+ </div>
158
+ ))}
159
+
160
+ <datalist id="known-keys">
161
+ {knownKeys.map((k) => <option key={k} value={k} />)}
162
+ </datalist>
163
+
164
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginTop: '1rem' }}>
165
+ <Button label="Cancel" className="p-button-text" onClick={onCancel} />
166
+ <Button label="Save" onClick={handleSave} />
167
+ </div>
168
+ </Dialog>
169
+ );
170
+ };
@@ -0,0 +1,131 @@
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 { TestFieldDialog } from './TestFieldDialog';
7
+ import type { TestField, TestMethod, FieldArrayKey } from '../types';
8
+
9
+ const TITLES: Record<FieldArrayKey, string> = {
10
+ project_fields: 'Project Fields',
11
+ config_fields: 'Config Fields',
12
+ cycle_fields: 'Cycle Fields',
13
+ results_fields: 'Results Fields',
14
+ };
15
+
16
+ const DESCRIPTIONS: Record<FieldArrayKey, string> = {
17
+ project_fields: 'System-level fields (operator, station). Filled by the HMI but not cycled.',
18
+ config_fields: 'Operator-input config (speeds, loads). Snapshotted into test.json on start.',
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.',
21
+ };
22
+
23
+ export interface FieldArrayEditorProps {
24
+ arrayKey: FieldArrayKey;
25
+ method: TestMethod;
26
+ onChange: (next: TestMethod) => void;
27
+ }
28
+
29
+ export const FieldArrayEditor: React.FC<FieldArrayEditorProps> = ({ arrayKey, method, onChange }) => {
30
+ const fields: TestField[] = (method[arrayKey] as TestField[]) ?? [];
31
+ const [dialogOpen, setDialogOpen] = useState(false);
32
+ const [editingIdx, setEditingIdx] = useState<number | null>(null);
33
+
34
+ const handleAdd = () => {
35
+ setEditingIdx(null);
36
+ setDialogOpen(true);
37
+ };
38
+
39
+ const handleEdit = (idx: number) => {
40
+ setEditingIdx(idx);
41
+ setDialogOpen(true);
42
+ };
43
+
44
+ const handleSaveField = (f: TestField) => {
45
+ const next = [...fields];
46
+ if (editingIdx === null) {
47
+ next.push(f);
48
+ } else {
49
+ next[editingIdx] = f;
50
+ }
51
+ onChange({ ...method, [arrayKey]: next });
52
+ setDialogOpen(false);
53
+ };
54
+
55
+ const handleRemove = (idx: number) => {
56
+ const target = fields[idx];
57
+ if (!target) return;
58
+ if (!window.confirm(`Remove field "${target.name}"?`)) return;
59
+ const next = fields.filter((_, i) => i !== idx);
60
+ onChange({ ...method, [arrayKey]: next });
61
+ };
62
+
63
+ const handleMove = (idx: number, dir: -1 | 1) => {
64
+ const j = idx + dir;
65
+ if (j < 0 || j >= fields.length) return;
66
+ const next = [...fields];
67
+ [next[idx], next[j]] = [next[j], next[idx]];
68
+ onChange({ ...method, [arrayKey]: next });
69
+ };
70
+
71
+ const rowActions = (_row: TestField, opts: { rowIndex: number }) => (
72
+ <div style={{ display: 'flex', gap: '0.25rem' }}>
73
+ <Button
74
+ icon="pi pi-arrow-up"
75
+ className="p-button-text p-button-sm"
76
+ disabled={opts.rowIndex === 0}
77
+ onClick={() => handleMove(opts.rowIndex, -1)}
78
+ aria-label="Move up"
79
+ />
80
+ <Button
81
+ icon="pi pi-arrow-down"
82
+ className="p-button-text p-button-sm"
83
+ disabled={opts.rowIndex === fields.length - 1}
84
+ onClick={() => handleMove(opts.rowIndex, 1)}
85
+ aria-label="Move down"
86
+ />
87
+ <Button
88
+ icon="pi pi-pencil"
89
+ className="p-button-text p-button-sm"
90
+ onClick={() => handleEdit(opts.rowIndex)}
91
+ aria-label="Edit"
92
+ />
93
+ <Button
94
+ icon="pi pi-trash"
95
+ className="p-button-text p-button-danger p-button-sm"
96
+ onClick={() => handleRemove(opts.rowIndex)}
97
+ aria-label="Remove"
98
+ />
99
+ </div>
100
+ );
101
+
102
+ return (
103
+ <>
104
+ <FormSection
105
+ title={TITLES[arrayKey]}
106
+ description={DESCRIPTIONS[arrayKey]}
107
+ actions={<Button label="Add field" icon="pi pi-plus" size="small" onClick={handleAdd} />}
108
+ >
109
+ <DataTable value={fields} dataKey="name" emptyMessage="No fields defined.">
110
+ <Column field="name" header="Name" />
111
+ <Column field="type" header="Type" style={{ width: '6rem' }} />
112
+ <Column field="units" header="Units" style={{ width: '6rem' }} />
113
+ <Column
114
+ header="Req"
115
+ body={(r: TestField) => r.required ? '✓' : ''}
116
+ style={{ width: '4rem' }}
117
+ />
118
+ <Column field="label" header="Label" />
119
+ <Column header="" body={rowActions} style={{ width: '10rem' }} />
120
+ </DataTable>
121
+ </FormSection>
122
+ <TestFieldDialog
123
+ visible={dialogOpen}
124
+ initial={editingIdx !== null ? (fields[editingIdx] ?? null) : null}
125
+ siblingNames={fields.map(f => f.name)}
126
+ onCancel={() => setDialogOpen(false)}
127
+ onSave={handleSaveField}
128
+ />
129
+ </>
130
+ );
131
+ };
@@ -0,0 +1,36 @@
1
+ import { InputText } from 'primereact/inputtext';
2
+ import { InputTextarea } from 'primereact/inputtextarea';
3
+ import { FormSection } from '../../forms/FormSection';
4
+ import { FormRow } from '../../forms/FormRow';
5
+ import type { TestMethod } from '../types';
6
+
7
+ export interface IdentitySectionProps {
8
+ method: TestMethod;
9
+ onChange: (next: TestMethod) => void;
10
+ }
11
+
12
+ export const IdentitySection: React.FC<IdentitySectionProps> = ({ method, onChange }) => {
13
+ return (
14
+ <FormSection
15
+ title="Identity"
16
+ description="Display label and operator-facing description for this method."
17
+ >
18
+ <FormRow label="Label" hint="Pretty name shown in the Test Method picker.">
19
+ <InputText
20
+ value={(method.label as string) ?? ''}
21
+ onChange={(e) => onChange({ ...method, label: e.target.value })}
22
+ />
23
+ </FormRow>
24
+ <FormRow
25
+ label="Description"
26
+ hint="Long-form guidance shown beneath the picker when this method is selected."
27
+ >
28
+ <InputTextarea
29
+ rows={3}
30
+ value={(method.description as string) ?? ''}
31
+ onChange={(e) => onChange({ ...method, description: e.target.value })}
32
+ />
33
+ </FormRow>
34
+ </FormSection>
35
+ );
36
+ };
@@ -0,0 +1,176 @@
1
+ /**
2
+ * MethodFormEditor — tabbed shell that composes the per-section subforms
3
+ * for a single TestMethod. The JSON tab is always available as an escape
4
+ * hatch for power users.
5
+ *
6
+ * Owns the local working copy of the method. Hands the changed copy back
7
+ * to the parent via onChange — the parent decides when to call put_method.
8
+ */
9
+
10
+ import { useEffect, useMemo, useState } from 'react';
11
+ import { TabView, TabPanel } from 'primereact/tabview';
12
+ import { Button } from 'primereact/button';
13
+ import { CodeEditor } from '../../CodeEditor';
14
+ import { IdentitySection } from './IdentitySection';
15
+ import { FieldArrayEditor } from './FieldArrayEditor';
16
+ import { ViewsEditor } from './ViewsEditor';
17
+ import { RawDataEditor } from './RawDataEditor';
18
+ import { AssetRefsEditor } from './AssetRefsEditor';
19
+ import { AnalysisEditor } from './AnalysisEditor';
20
+ import { validateMethod } from '../validation';
21
+ import type { TestMethod } from '../types';
22
+
23
+ export interface MethodFormEditorProps {
24
+ methodId: string;
25
+ method: TestMethod;
26
+ /** Called on Apply with the working copy — parent forwards to put_method. */
27
+ onApply: (next: TestMethod) => Promise<void> | void;
28
+ /** AMS asset types — passed through to AssetRefsEditor for the dropdown. */
29
+ knownAssetTypes?: string[];
30
+ busy?: boolean;
31
+ }
32
+
33
+ export const MethodFormEditor: React.FC<MethodFormEditorProps> = ({
34
+ methodId, method, onApply, knownAssetTypes = [], busy,
35
+ }) => {
36
+ const [draft, setDraft] = useState<TestMethod>(method);
37
+ const [jsonText, setJsonText] = useState<string>('');
38
+ const [jsonError, setJsonError] = useState<string | null>(null);
39
+ const [activeTab, setActiveTab] = useState<number>(0);
40
+
41
+ // Reset the working copy whenever the incoming method changes — i.e.
42
+ // the user selected a different method, or the server returned a fresh
43
+ // version after a save. Unapplied tab edits are discarded.
44
+ useEffect(() => {
45
+ setDraft(method);
46
+ setJsonText(JSON.stringify(method, null, 2));
47
+ setJsonError(null);
48
+ }, [method, methodId]);
49
+
50
+ const dirty = useMemo(() => {
51
+ try {
52
+ return JSON.stringify(draft) !== JSON.stringify(method);
53
+ } catch {
54
+ return true;
55
+ }
56
+ }, [draft, method]);
57
+
58
+ // Live cross-field validation. Mirrors the server's validate_method —
59
+ // gives the operator immediate feedback in the form, not just after Apply.
60
+ const validationErrors = useMemo(() => validateMethod(methodId, draft), [draft, methodId]);
61
+
62
+ const handleFormChange = (next: TestMethod) => {
63
+ setDraft(next);
64
+ // Keep the JSON tab's text in sync so switching tabs doesn't show
65
+ // stale JSON. This is one-way (form → JSON); the reverse happens
66
+ // only when the user explicitly types in the JSON tab.
67
+ setJsonText(JSON.stringify(next, null, 2));
68
+ };
69
+
70
+ const handleJsonChange = (v: string | undefined) => {
71
+ const text = v ?? '';
72
+ setJsonText(text);
73
+ try {
74
+ const parsed = JSON.parse(text);
75
+ setDraft(parsed);
76
+ setJsonError(null);
77
+ } catch (e: any) {
78
+ setJsonError(String(e?.message ?? e));
79
+ // Don't update draft on parse error — form tabs stay valid.
80
+ }
81
+ };
82
+
83
+ const handleApply = async () => {
84
+ // If the JSON tab is active and has a parse error, refuse.
85
+ if (jsonError) return;
86
+ await onApply(draft);
87
+ };
88
+
89
+ return (
90
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
91
+ <div
92
+ style={{
93
+ display: 'flex',
94
+ justifyContent: 'space-between',
95
+ alignItems: 'center',
96
+ padding: '0.5rem 1rem',
97
+ borderBottom: '1px solid var(--surface-d, #e2e8f0)',
98
+ background: 'var(--surface-b, #f8fafc)',
99
+ }}
100
+ >
101
+ <strong>
102
+ {methodId}
103
+ {dirty && <span style={{ marginLeft: '0.5rem', color: '#ea580c' }}>•</span>}
104
+ {validationErrors.length > 0 && (
105
+ <span
106
+ title={validationErrors.map(e => e.message).join('\n')}
107
+ style={{
108
+ marginLeft: '0.5rem',
109
+ background: '#fef2f2',
110
+ color: '#991b1b',
111
+ padding: '0.1rem 0.5rem',
112
+ borderRadius: 4,
113
+ fontSize: '0.75rem',
114
+ fontWeight: 600,
115
+ cursor: 'help',
116
+ }}
117
+ >
118
+ {validationErrors.length} issue{validationErrors.length === 1 ? '' : 's'}
119
+ </span>
120
+ )}
121
+ </strong>
122
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
123
+ <Button
124
+ label="Apply"
125
+ icon="pi pi-check"
126
+ disabled={busy || !dirty || !!jsonError || validationErrors.length > 0}
127
+ onClick={handleApply}
128
+ />
129
+ </div>
130
+ </div>
131
+
132
+ <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
133
+ <TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}>
134
+ <TabPanel header="Identity">
135
+ <IdentitySection method={draft} onChange={handleFormChange} />
136
+ </TabPanel>
137
+ <TabPanel header="Fields">
138
+ <FieldArrayEditor arrayKey="project_fields" method={draft} onChange={handleFormChange} />
139
+ <FieldArrayEditor arrayKey="config_fields" method={draft} onChange={handleFormChange} />
140
+ <FieldArrayEditor arrayKey="cycle_fields" method={draft} onChange={handleFormChange} />
141
+ <FieldArrayEditor arrayKey="results_fields" method={draft} onChange={handleFormChange} />
142
+ </TabPanel>
143
+ <TabPanel header="Views">
144
+ <ViewsEditor method={draft} onChange={handleFormChange} />
145
+ </TabPanel>
146
+ <TabPanel header="Raw Data">
147
+ <RawDataEditor method={draft} onChange={handleFormChange} />
148
+ </TabPanel>
149
+ <TabPanel header="Assets">
150
+ <AssetRefsEditor method={draft} onChange={handleFormChange} knownAssetTypes={knownAssetTypes} />
151
+ </TabPanel>
152
+ <TabPanel header="Analysis">
153
+ <AnalysisEditor method={draft} onChange={handleFormChange} />
154
+ </TabPanel>
155
+ <TabPanel header="JSON">
156
+ {jsonError && (
157
+ <div style={{ color: '#dc2626', marginBottom: '0.5rem', fontFamily: 'monospace' }}>
158
+ {jsonError}
159
+ </div>
160
+ )}
161
+ <div style={{ height: '60vh' }}>
162
+ <CodeEditor
163
+ code={jsonText}
164
+ language="json"
165
+ theme="vs-dark"
166
+ readOnly={false}
167
+ cursorStyle="line"
168
+ onCodeChanged={handleJsonChange}
169
+ />
170
+ </div>
171
+ </TabPanel>
172
+ </TabView>
173
+ </div>
174
+ </div>
175
+ );
176
+ };
@@ -0,0 +1,117 @@
1
+ import { InputText } from 'primereact/inputtext';
2
+ import { Button } from 'primereact/button';
3
+ import { Checkbox } from 'primereact/checkbox';
4
+ import { FormSection } from '../../forms/FormSection';
5
+ import { FormRow } from '../../forms/FormRow';
6
+ import type { RawColumn, RawDataShape, TestMethod } from '../types';
7
+
8
+ export interface RawDataEditorProps {
9
+ method: TestMethod;
10
+ onChange: (next: TestMethod) => void;
11
+ }
12
+
13
+ const emptyRawData = (): RawDataShape => ({ blob_name: 'trace', columns: {} });
14
+
15
+ export const RawDataEditor: React.FC<RawDataEditorProps> = ({ method, onChange }) => {
16
+ const rd = method.raw_data as RawDataShape | null | undefined;
17
+ const enabled = !!rd;
18
+
19
+ const setRawData = (next: RawDataShape | null) => {
20
+ onChange({ ...method, raw_data: next });
21
+ };
22
+
23
+ const updateColumn = (name: string, col: RawColumn) => {
24
+ if (!rd) return;
25
+ setRawData({ ...rd, columns: { ...rd.columns, [name]: col } });
26
+ };
27
+ const renameColumn = (oldName: string, newName: string) => {
28
+ if (!rd || !newName.trim() || newName === oldName) return;
29
+ if (rd.columns[newName]) return;
30
+ const cols = { ...rd.columns };
31
+ cols[newName] = cols[oldName];
32
+ delete cols[oldName];
33
+ setRawData({ ...rd, columns: cols });
34
+ };
35
+ const removeColumn = (name: string) => {
36
+ if (!rd) return;
37
+ if (!window.confirm(`Remove column "${name}"?`)) return;
38
+ const cols = { ...rd.columns };
39
+ delete cols[name];
40
+ setRawData({ ...rd, columns: cols });
41
+ };
42
+ const addColumn = () => {
43
+ if (!rd) return;
44
+ let base = 'column';
45
+ let n = 1;
46
+ while (rd.columns[`${base}${n}`]) n++;
47
+ setRawData({ ...rd, columns: { ...rd.columns, [`${base}${n}`]: { source: 'time' } } });
48
+ };
49
+
50
+ return (
51
+ <FormSection
52
+ title="Raw Data"
53
+ description="Columnar blob shape captured per cycle under raw_data/. Required only when the method records DAQ traces."
54
+ actions={
55
+ <Checkbox
56
+ checked={enabled}
57
+ onChange={(e) => setRawData(e.checked ? emptyRawData() : null)}
58
+ />
59
+ }
60
+ >
61
+ {!enabled && <small>Raw data capture disabled. Tick the box above to enable.</small>}
62
+ {enabled && rd && (
63
+ <>
64
+ <FormRow label="Blob name" required hint="Base filename under raw_data/, no extension.">
65
+ <InputText
66
+ value={rd.blob_name}
67
+ onChange={(e) => setRawData({ ...rd, blob_name: e.target.value })}
68
+ />
69
+ </FormRow>
70
+
71
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '0.75rem' }}>
72
+ <strong>Columns</strong>
73
+ <Button label="Add column" icon="pi pi-plus" size="small" onClick={addColumn} />
74
+ </div>
75
+ {Object.keys(rd.columns).length === 0 && <small>No columns defined.</small>}
76
+ {Object.entries(rd.columns).map(([name, col]) => (
77
+ <div
78
+ key={name}
79
+ style={{
80
+ display: 'grid',
81
+ gridTemplateColumns: '1fr 1.5fr 1.5fr 2rem',
82
+ gap: '0.5rem',
83
+ marginTop: '0.25rem',
84
+ alignItems: 'center',
85
+ }}
86
+ >
87
+ <InputText
88
+ placeholder="column name"
89
+ defaultValue={name}
90
+ onBlur={(e) => renameColumn(name, e.target.value.trim())}
91
+ />
92
+ <InputText
93
+ placeholder="source (time / ni.<daq>.channels.<name> / derived)"
94
+ value={col.source}
95
+ onChange={(e) => updateColumn(name, { ...col, source: e.target.value })}
96
+ />
97
+ <InputText
98
+ placeholder="formula (only when source=derived)"
99
+ value={col.formula ?? ''}
100
+ onChange={(e) =>
101
+ updateColumn(name, { ...col, formula: e.target.value || undefined })
102
+ }
103
+ disabled={col.source !== 'derived'}
104
+ />
105
+ <Button
106
+ icon="pi pi-trash"
107
+ className="p-button-text p-button-danger p-button-sm"
108
+ onClick={() => removeColumn(name)}
109
+ aria-label="Remove column"
110
+ />
111
+ </div>
112
+ ))}
113
+ </>
114
+ )}
115
+ </FormSection>
116
+ );
117
+ };