@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.
- package/dist/components/ValueInput.css +9 -12
- package/dist/components/ValueInput.d.ts +45 -154
- package/dist/components/ValueInput.d.ts.map +1 -1
- package/dist/components/ValueInput.js +1 -1
- package/dist/components/ams/AssetEditDialog.d.ts.map +1 -1
- package/dist/components/ams/AssetEditDialog.js +1 -1
- package/dist/components/forms/FormRow.d.ts +20 -0
- package/dist/components/forms/FormRow.d.ts.map +1 -0
- package/dist/components/forms/FormRow.js +1 -0
- package/dist/components/forms/FormSection.d.ts +19 -0
- package/dist/components/forms/FormSection.d.ts.map +1 -0
- package/dist/components/forms/FormSection.js +1 -0
- package/dist/components/forms/forms.css +89 -0
- package/dist/components/forms/index.d.ts +3 -0
- package/dist/components/forms/index.d.ts.map +1 -0
- package/dist/components/forms/index.js +1 -0
- package/dist/components/tis-editor/TisConfigEditor.css +121 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts +28 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/TisConfigEditor.js +1 -0
- package/dist/components/tis-editor/editor/AnalysisEditor.d.ts +7 -0
- package/dist/components/tis-editor/editor/AnalysisEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/AnalysisEditor.js +1 -0
- package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts +10 -0
- package/dist/components/tis-editor/editor/AssetRefsEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/AssetRefsEditor.js +1 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +16 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts +8 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -0
- package/dist/components/tis-editor/editor/IdentitySection.d.ts +7 -0
- package/dist/components/tis-editor/editor/IdentitySection.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/IdentitySection.js +1 -0
- package/dist/components/tis-editor/editor/MethodFormEditor.d.ts +20 -0
- package/dist/components/tis-editor/editor/MethodFormEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/MethodFormEditor.js +1 -0
- package/dist/components/tis-editor/editor/RawDataEditor.d.ts +7 -0
- package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/RawDataEditor.js +1 -0
- package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts +22 -0
- package/dist/components/tis-editor/editor/SaveDiffDialog.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/SaveDiffDialog.js +1 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +11 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -0
- package/dist/components/tis-editor/editor/ViewsEditor.d.ts +7 -0
- package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/ViewsEditor.js +1 -0
- package/dist/components/tis-editor/types.d.ts +78 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -0
- package/dist/components/tis-editor/types.js +1 -0
- package/dist/components/tis-editor/validation.d.ts +20 -0
- package/dist/components/tis-editor/validation.d.ts.map +1 -0
- package/dist/components/tis-editor/validation.js +1 -0
- package/dist/hooks/useAmsAssetTypes.d.ts +23 -0
- package/dist/hooks/useAmsAssetTypes.d.ts.map +1 -0
- package/dist/hooks/useAmsAssetTypes.js +1 -0
- package/dist/hooks/useTisConfig.d.ts +51 -0
- package/dist/hooks/useTisConfig.d.ts.map +1 -0
- package/dist/hooks/useTisConfig.js +1 -0
- package/package.json +9 -3
- package/src/components/ValueInput.css +9 -12
- package/src/components/ValueInput.tsx +132 -317
- package/src/components/ams/AssetEditDialog.tsx +357 -20
- package/src/components/forms/FormRow.tsx +37 -0
- package/src/components/forms/FormSection.tsx +39 -0
- package/src/components/forms/forms.css +89 -0
- package/src/components/forms/index.ts +2 -0
- package/src/components/tis-editor/TisConfigEditor.css +121 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +321 -0
- package/src/components/tis-editor/editor/AnalysisEditor.tsx +54 -0
- package/src/components/tis-editor/editor/AssetRefsEditor.tsx +187 -0
- package/src/components/tis-editor/editor/ChartViewDialog.tsx +170 -0
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +131 -0
- package/src/components/tis-editor/editor/IdentitySection.tsx +36 -0
- package/src/components/tis-editor/editor/MethodFormEditor.tsx +176 -0
- package/src/components/tis-editor/editor/RawDataEditor.tsx +117 -0
- package/src/components/tis-editor/editor/SaveDiffDialog.tsx +160 -0
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +134 -0
- package/src/components/tis-editor/editor/ViewsEditor.tsx +101 -0
- package/src/components/tis-editor/types.ts +95 -0
- package/src/components/tis-editor/validation.ts +104 -0
- package/src/hooks/useAmsAssetTypes.ts +70 -0
- 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
|
+
};
|