@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,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
|
+
};
|
|
@@ -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
|
+
};
|