@adcops/autocore-react 3.3.85 → 3.3.89
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/assets/AxisC.d.ts +4 -0
- package/dist/assets/AxisC.d.ts.map +1 -0
- package/dist/assets/AxisC.js +1 -0
- package/dist/assets/AxisX.js +1 -1
- package/dist/assets/AxisY.js +1 -1
- package/dist/assets/AxisZ.js +1 -1
- 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/AmsProvider.d.ts +10 -0
- package/dist/components/ams/AmsProvider.d.ts.map +1 -1
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
- package/dist/components/ams/AssetRegistryTable.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/assets/AxisC.tsx +38 -0
- package/src/assets/AxisX.tsx +32 -32
- package/src/assets/AxisY.tsx +34 -34
- package/src/assets/AxisZ.tsx +31 -31
- package/src/components/ValueInput.css +9 -12
- package/src/components/ValueInput.tsx +132 -317
- package/src/components/ams/AmsProvider.tsx +10 -0
- package/src/components/ams/AssetRegistryTable.tsx +53 -8
- 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,187 @@
|
|
|
1
|
+
import { 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 { DataTable } from 'primereact/datatable';
|
|
8
|
+
import { Column } from 'primereact/column';
|
|
9
|
+
import { FormSection } from '../../forms/FormSection';
|
|
10
|
+
import { FormRow } from '../../forms/FormRow';
|
|
11
|
+
import type { AssetRef, TestMethod } from '../types';
|
|
12
|
+
|
|
13
|
+
const SELECT_OPTIONS = [
|
|
14
|
+
{ label: 'By location', value: 'by_location' },
|
|
15
|
+
{ label: 'By id field', value: 'by_id_field' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const CALIBRATION_OPTIONS = [
|
|
19
|
+
{ label: 'Ignore', value: 'ignore' },
|
|
20
|
+
{ label: 'Warn (default)', value: 'warn' },
|
|
21
|
+
{ label: 'Require', value: 'require' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export interface AssetRefsEditorProps {
|
|
25
|
+
method: TestMethod;
|
|
26
|
+
onChange: (next: TestMethod) => void;
|
|
27
|
+
/** Asset types known to AMS, supplied by the host. Empty array = use a
|
|
28
|
+
* free-form text field. Phase 3 wires this up to ams.list_schemas. */
|
|
29
|
+
knownAssetTypes?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const blank = (): AssetRef => ({
|
|
33
|
+
field: '', asset_type: '', select: 'by_location',
|
|
34
|
+
calibration_required: 'warn',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const AssetRefsEditor: React.FC<AssetRefsEditorProps> = ({ method, onChange, knownAssetTypes = [] }) => {
|
|
38
|
+
const refs: AssetRef[] = (method.asset_refs as AssetRef[]) ?? [];
|
|
39
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
40
|
+
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
|
41
|
+
const [draft, setDraft] = useState<AssetRef>(blank());
|
|
42
|
+
const [error, setError] = useState<string | null>(null);
|
|
43
|
+
|
|
44
|
+
const openNew = () => {
|
|
45
|
+
setEditingIdx(null);
|
|
46
|
+
setDraft(blank());
|
|
47
|
+
setError(null);
|
|
48
|
+
setDialogOpen(true);
|
|
49
|
+
};
|
|
50
|
+
const openEdit = (i: number) => {
|
|
51
|
+
setEditingIdx(i);
|
|
52
|
+
setDraft({ ...refs[i] });
|
|
53
|
+
setError(null);
|
|
54
|
+
setDialogOpen(true);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const validate = (a: AssetRef): string | null => {
|
|
58
|
+
if (!a.field.trim()) return 'Field name is required.';
|
|
59
|
+
if (!a.asset_type.trim()) return 'Asset type is required.';
|
|
60
|
+
if (a.select === 'by_location' && !a.location?.trim()) {
|
|
61
|
+
return 'Location is required when select=by_location.';
|
|
62
|
+
}
|
|
63
|
+
if (a.select === 'by_id_field' && !a.from?.trim()) {
|
|
64
|
+
return 'From-path is required when select=by_id_field.';
|
|
65
|
+
}
|
|
66
|
+
const dupOfOther = refs.some((r, i) => r.field === a.field && i !== editingIdx);
|
|
67
|
+
if (dupOfOther) return `An asset_ref for field "${a.field}" already exists.`;
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleSave = () => {
|
|
72
|
+
const err = validate(draft);
|
|
73
|
+
if (err) { setError(err); return; }
|
|
74
|
+
const next = [...refs];
|
|
75
|
+
if (editingIdx === null) next.push(draft);
|
|
76
|
+
else next[editingIdx] = draft;
|
|
77
|
+
onChange({ ...method, asset_refs: next });
|
|
78
|
+
setDialogOpen(false);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleRemove = (i: number) => {
|
|
82
|
+
if (!window.confirm(`Remove asset_ref "${refs[i].field}"?`)) return;
|
|
83
|
+
onChange({ ...method, asset_refs: refs.filter((_, idx) => idx !== i) });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const rowActions = (_r: AssetRef, opts: { rowIndex: number }) => (
|
|
87
|
+
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
88
|
+
<Button icon="pi pi-pencil" className="p-button-text p-button-sm" onClick={() => openEdit(opts.rowIndex)} />
|
|
89
|
+
<Button icon="pi pi-trash" className="p-button-text p-button-danger p-button-sm" onClick={() => handleRemove(opts.rowIndex)} />
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const assetTypeOptions = knownAssetTypes.length > 0
|
|
94
|
+
? knownAssetTypes.map(t => ({ label: t, value: t }))
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<FormSection
|
|
100
|
+
title="Asset References"
|
|
101
|
+
description="AMS dependencies resolved at start_test time and snapshotted into test.json."
|
|
102
|
+
actions={<Button label="Add asset_ref" icon="pi pi-plus" size="small" onClick={openNew} />}
|
|
103
|
+
>
|
|
104
|
+
<DataTable value={refs} dataKey="field" emptyMessage="No asset_refs declared.">
|
|
105
|
+
<Column field="field" header="Field" />
|
|
106
|
+
<Column field="asset_type" header="Asset type" />
|
|
107
|
+
<Column field="select" header="Select" style={{ width: '7rem' }} />
|
|
108
|
+
<Column
|
|
109
|
+
header="Locator"
|
|
110
|
+
body={(r: AssetRef) => r.select === 'by_location' ? r.location : r.from}
|
|
111
|
+
/>
|
|
112
|
+
<Column field="calibration_required" header="Calibration" style={{ width: '7rem' }} />
|
|
113
|
+
<Column header="" body={rowActions} style={{ width: '6rem' }} />
|
|
114
|
+
</DataTable>
|
|
115
|
+
</FormSection>
|
|
116
|
+
|
|
117
|
+
<Dialog
|
|
118
|
+
header={editingIdx === null ? 'New asset_ref' : `Edit asset_ref: ${refs[editingIdx ?? 0]?.field}`}
|
|
119
|
+
visible={dialogOpen}
|
|
120
|
+
onHide={() => setDialogOpen(false)}
|
|
121
|
+
style={{ width: '40rem' }}
|
|
122
|
+
>
|
|
123
|
+
{error && <div style={{ color: '#dc2626', marginBottom: '0.5rem' }}>{error}</div>}
|
|
124
|
+
<FormRow label="Field" required hint="Key under test.json::asset_snapshot.">
|
|
125
|
+
<InputText value={draft.field} onChange={(e) => setDraft({ ...draft, field: e.target.value })} />
|
|
126
|
+
</FormRow>
|
|
127
|
+
<FormRow label="Asset type" required>
|
|
128
|
+
{assetTypeOptions ? (
|
|
129
|
+
<Dropdown
|
|
130
|
+
value={draft.asset_type}
|
|
131
|
+
options={assetTypeOptions}
|
|
132
|
+
onChange={(e) => setDraft({ ...draft, asset_type: e.value })}
|
|
133
|
+
editable
|
|
134
|
+
/>
|
|
135
|
+
) : (
|
|
136
|
+
<InputText
|
|
137
|
+
value={draft.asset_type}
|
|
138
|
+
onChange={(e) => setDraft({ ...draft, asset_type: e.target.value })}
|
|
139
|
+
placeholder="e.g. load_cell"
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
</FormRow>
|
|
143
|
+
<FormRow label="Select" required>
|
|
144
|
+
<Dropdown
|
|
145
|
+
value={draft.select}
|
|
146
|
+
options={SELECT_OPTIONS}
|
|
147
|
+
onChange={(e) => setDraft({ ...draft, select: e.value })}
|
|
148
|
+
/>
|
|
149
|
+
</FormRow>
|
|
150
|
+
{draft.select === 'by_location' && (
|
|
151
|
+
<FormRow label="Location" required hint="AMS location key (e.g. tsdr).">
|
|
152
|
+
<InputText value={draft.location ?? ''} onChange={(e) => setDraft({ ...draft, location: e.target.value })} />
|
|
153
|
+
</FormRow>
|
|
154
|
+
)}
|
|
155
|
+
{draft.select === 'by_id_field' && (
|
|
156
|
+
<FormRow label="From" required hint="Dotted config path (e.g. config.surface_asset_id).">
|
|
157
|
+
<InputText value={draft.from ?? ''} onChange={(e) => setDraft({ ...draft, from: e.target.value })} />
|
|
158
|
+
</FormRow>
|
|
159
|
+
)}
|
|
160
|
+
<FormRow label="Calibration policy">
|
|
161
|
+
<Dropdown
|
|
162
|
+
value={draft.calibration_required ?? 'warn'}
|
|
163
|
+
options={CALIBRATION_OPTIONS}
|
|
164
|
+
onChange={(e) => setDraft({ ...draft, calibration_required: e.value })}
|
|
165
|
+
/>
|
|
166
|
+
</FormRow>
|
|
167
|
+
<FormRow label="Label">
|
|
168
|
+
<InputText
|
|
169
|
+
value={draft.label ?? ''}
|
|
170
|
+
onChange={(e) => setDraft({ ...draft, label: e.target.value || undefined })}
|
|
171
|
+
/>
|
|
172
|
+
</FormRow>
|
|
173
|
+
<FormRow label="Description">
|
|
174
|
+
<InputTextarea
|
|
175
|
+
rows={2}
|
|
176
|
+
value={draft.description ?? ''}
|
|
177
|
+
onChange={(e) => setDraft({ ...draft, description: e.target.value || undefined })}
|
|
178
|
+
/>
|
|
179
|
+
</FormRow>
|
|
180
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginTop: '1rem' }}>
|
|
181
|
+
<Button label="Cancel" className="p-button-text" onClick={() => setDialogOpen(false)} />
|
|
182
|
+
<Button label="Save" onClick={handleSave} />
|
|
183
|
+
</div>
|
|
184
|
+
</Dialog>
|
|
185
|
+
</>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
@@ -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
|
+
};
|