@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.
- 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/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/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,121 @@
|
|
|
1
|
+
.tis-editor {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: 100%;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.tis-editor__header {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: space-between;
|
|
12
|
+
padding: 0.75rem 1rem;
|
|
13
|
+
border-bottom: 1px solid var(--surface-d, #e2e8f0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.tis-editor__header h2 {
|
|
17
|
+
margin: 0;
|
|
18
|
+
font-size: 1.125rem;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.tis-editor__header-actions {
|
|
22
|
+
display: flex;
|
|
23
|
+
gap: 0.5rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tis-editor__dirty-pill {
|
|
27
|
+
display: inline-block;
|
|
28
|
+
background: #ea580c;
|
|
29
|
+
color: white;
|
|
30
|
+
font-size: 0.7rem;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
text-transform: uppercase;
|
|
33
|
+
letter-spacing: 0.05em;
|
|
34
|
+
padding: 0.15rem 0.5rem;
|
|
35
|
+
border-radius: 999px;
|
|
36
|
+
margin-left: 0.5rem;
|
|
37
|
+
vertical-align: middle;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.tis-editor__body {
|
|
41
|
+
flex: 1;
|
|
42
|
+
display: flex;
|
|
43
|
+
min-height: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.tis-editor__sidebar {
|
|
47
|
+
flex: 0 0 320px;
|
|
48
|
+
border-right: 1px solid var(--surface-d, #e2e8f0);
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
min-height: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.tis-editor__sidebar-actions {
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 0.25rem;
|
|
57
|
+
padding: 0.5rem;
|
|
58
|
+
border-bottom: 1px solid var(--surface-d, #e2e8f0);
|
|
59
|
+
flex-wrap: wrap;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.tis-editor__sidebar-actions .p-button {
|
|
63
|
+
flex: 1 1 auto;
|
|
64
|
+
min-width: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.tis-editor__detail {
|
|
68
|
+
flex: 1;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
min-height: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.tis-editor__detail-header {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: space-between;
|
|
78
|
+
padding: 0.5rem 1rem;
|
|
79
|
+
border-bottom: 1px solid var(--surface-d, #e2e8f0);
|
|
80
|
+
background: var(--surface-b, #f8fafc);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.tis-editor__editor-wrap {
|
|
84
|
+
flex: 1;
|
|
85
|
+
min-height: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.tis-editor__editor-wrap > * {
|
|
89
|
+
height: 100%;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.tis-editor__error {
|
|
93
|
+
background: #fef2f2;
|
|
94
|
+
color: #991b1b;
|
|
95
|
+
border-left: 3px solid #dc2626;
|
|
96
|
+
padding: 0.5rem 1rem;
|
|
97
|
+
margin: 0.5rem 1rem;
|
|
98
|
+
font-size: 0.875rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.tis-editor__error pre {
|
|
102
|
+
white-space: pre-wrap;
|
|
103
|
+
margin: 0.25rem 0 0 0;
|
|
104
|
+
font-family: ui-monospace, monospace;
|
|
105
|
+
font-size: 0.8rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.tis-editor__empty {
|
|
109
|
+
flex: 1;
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
color: var(--text-color-secondary, #64748b);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.tis-editor__new-method-label {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
gap: 0.25rem;
|
|
120
|
+
margin-bottom: 0.25rem;
|
|
121
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TisConfigEditor — master-detail UI for editing TIS test_methods.
|
|
3
|
+
*
|
|
4
|
+
* Left: PrimeReact DataTable listing methods (method_id + label).
|
|
5
|
+
* Right: tabbed editor for the selected method. Phase 1 ships a single
|
|
6
|
+
* JSON-via-Monaco tab; Phase 2 layers form editors on top of it.
|
|
7
|
+
* Action bar: New / Duplicate / Delete / Apply / Save / Revert.
|
|
8
|
+
*
|
|
9
|
+
* "Apply" pushes the local Monaco buffer to the server-side stage
|
|
10
|
+
* (`tis.put_method`). "Save" persists the entire stage to project.json
|
|
11
|
+
* (`tis.save_config`). "Revert" drops the stage (`tis.discard_config_changes`).
|
|
12
|
+
*
|
|
13
|
+
* Save is disabled (with a tooltip) when active tests are open against
|
|
14
|
+
* any method — the server enforces the same gate, but disabling on the
|
|
15
|
+
* client gives clearer UX.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
19
|
+
import { DataTable } from 'primereact/datatable';
|
|
20
|
+
import { Column } from 'primereact/column';
|
|
21
|
+
import { Button } from 'primereact/button';
|
|
22
|
+
import { InputText } from 'primereact/inputtext';
|
|
23
|
+
import { Dialog } from 'primereact/dialog';
|
|
24
|
+
import { useContext } from 'react';
|
|
25
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
26
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
27
|
+
import { useTisConfig, type TisIpcInvoker } from '../../hooks/useTisConfig';
|
|
28
|
+
import { useAmsAssetTypes } from '../../hooks/useAmsAssetTypes';
|
|
29
|
+
import { MethodFormEditor } from './editor/MethodFormEditor';
|
|
30
|
+
import { SaveDiffDialog } from './editor/SaveDiffDialog';
|
|
31
|
+
import type { TestMethod } from './types';
|
|
32
|
+
|
|
33
|
+
import './TisConfigEditor.css';
|
|
34
|
+
|
|
35
|
+
export interface TisConfigEditorProps {
|
|
36
|
+
/** Project ID. Today this is informational (the server hosts one
|
|
37
|
+
* project) but the wire contract carries it for forward compatibility. */
|
|
38
|
+
projectId: string;
|
|
39
|
+
/** Optional invoker override — primarily for the playground / tests. */
|
|
40
|
+
invoker?: TisIpcInvoker;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface MethodRow {
|
|
44
|
+
id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const EMPTY_METHOD: TestMethod = {
|
|
49
|
+
label: '',
|
|
50
|
+
description: '',
|
|
51
|
+
project_fields: [],
|
|
52
|
+
config_fields: [],
|
|
53
|
+
cycle_fields: [],
|
|
54
|
+
results_fields: [],
|
|
55
|
+
views: {},
|
|
56
|
+
asset_refs: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const TisConfigEditor: React.FC<TisConfigEditorProps> = ({ projectId, invoker }) => {
|
|
60
|
+
const ctx = useContext(EventEmitterContext);
|
|
61
|
+
// Resolve invoker once so it can be passed into the SaveDiffDialog
|
|
62
|
+
// alongside the hook's internal use of it.
|
|
63
|
+
const effectiveInvoker: TisIpcInvoker = invoker
|
|
64
|
+
?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any));
|
|
65
|
+
|
|
66
|
+
const tis = useTisConfig(projectId, { invoker: effectiveInvoker });
|
|
67
|
+
const ams = useAmsAssetTypes({ invoker: effectiveInvoker });
|
|
68
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
69
|
+
const [draftError, setDraftError] = useState<string | null>(null);
|
|
70
|
+
const [busy, setBusy] = useState<boolean>(false);
|
|
71
|
+
const [saveDialogOpen, setSaveDialogOpen] = useState<boolean>(false);
|
|
72
|
+
|
|
73
|
+
// New-method dialog state.
|
|
74
|
+
const [newDialogOpen, setNewDialogOpen] = useState<boolean>(false);
|
|
75
|
+
const [newId, setNewId] = useState<string>('');
|
|
76
|
+
|
|
77
|
+
const rows: MethodRow[] = useMemo(() => {
|
|
78
|
+
if (!tis.config) return [];
|
|
79
|
+
return Object.entries(tis.config.methods).map(([id, m]) => ({
|
|
80
|
+
id,
|
|
81
|
+
label: (m as any)?.label ?? '',
|
|
82
|
+
}));
|
|
83
|
+
}, [tis.config]);
|
|
84
|
+
|
|
85
|
+
// Auto-select the default method on first successful load.
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!selectedId && tis.config) {
|
|
88
|
+
const fallback = tis.config.defaultMethodId
|
|
89
|
+
|| Object.keys(tis.config.methods)[0]
|
|
90
|
+
|| null;
|
|
91
|
+
setSelectedId(fallback);
|
|
92
|
+
}
|
|
93
|
+
}, [tis.config, selectedId]);
|
|
94
|
+
|
|
95
|
+
// Clear errors whenever the selected method changes.
|
|
96
|
+
useEffect(() => { setDraftError(null); }, [selectedId]);
|
|
97
|
+
|
|
98
|
+
const onApply = async (next: TestMethod) => {
|
|
99
|
+
if (!selectedId) return;
|
|
100
|
+
setBusy(true);
|
|
101
|
+
try {
|
|
102
|
+
await tis.putMethod(selectedId, next as any);
|
|
103
|
+
setDraftError(null);
|
|
104
|
+
} catch (e: any) {
|
|
105
|
+
setDraftError(String(e?.message ?? e));
|
|
106
|
+
} finally {
|
|
107
|
+
setBusy(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const onCreate = async () => {
|
|
112
|
+
const id = newId.trim();
|
|
113
|
+
if (!id) return;
|
|
114
|
+
if (tis.config?.methods[id]) {
|
|
115
|
+
setDraftError(`A method named "${id}" already exists.`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setBusy(true);
|
|
119
|
+
try {
|
|
120
|
+
await tis.putMethod(id, EMPTY_METHOD);
|
|
121
|
+
setSelectedId(id);
|
|
122
|
+
setNewDialogOpen(false);
|
|
123
|
+
setNewId('');
|
|
124
|
+
} catch (e: any) {
|
|
125
|
+
setDraftError(String(e?.message ?? e));
|
|
126
|
+
} finally {
|
|
127
|
+
setBusy(false);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const onDuplicate = async () => {
|
|
132
|
+
if (!selectedId || !tis.config) return;
|
|
133
|
+
const source = tis.config.methods[selectedId];
|
|
134
|
+
if (!source) return;
|
|
135
|
+
let candidate = `${selectedId}_copy`;
|
|
136
|
+
let n = 2;
|
|
137
|
+
while (tis.config.methods[candidate]) {
|
|
138
|
+
candidate = `${selectedId}_copy_${n++}`;
|
|
139
|
+
}
|
|
140
|
+
setBusy(true);
|
|
141
|
+
try {
|
|
142
|
+
await tis.putMethod(candidate, JSON.parse(JSON.stringify(source)));
|
|
143
|
+
setSelectedId(candidate);
|
|
144
|
+
} catch (e: any) {
|
|
145
|
+
setDraftError(String(e?.message ?? e));
|
|
146
|
+
} finally {
|
|
147
|
+
setBusy(false);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const onDelete = async () => {
|
|
152
|
+
if (!selectedId) return;
|
|
153
|
+
if (!window.confirm(`Remove method "${selectedId}"? This is staged — Save persists it.`)) return;
|
|
154
|
+
setBusy(true);
|
|
155
|
+
try {
|
|
156
|
+
await tis.removeMethod(selectedId);
|
|
157
|
+
setSelectedId(null);
|
|
158
|
+
} catch (e: any) {
|
|
159
|
+
setDraftError(String(e?.message ?? e));
|
|
160
|
+
} finally {
|
|
161
|
+
setBusy(false);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Save flows through the diff dialog: it fetches the current disk
|
|
166
|
+
// state via tis.list_schemas, shows the operator what's about to land,
|
|
167
|
+
// and only invokes save_config on confirm.
|
|
168
|
+
const openSaveDialog = () => setSaveDialogOpen(true);
|
|
169
|
+
const onSaveConfirm = async () => {
|
|
170
|
+
setBusy(true);
|
|
171
|
+
try {
|
|
172
|
+
await tis.save();
|
|
173
|
+
setSaveDialogOpen(false);
|
|
174
|
+
} catch (e: any) {
|
|
175
|
+
setDraftError(String(e?.message ?? e));
|
|
176
|
+
} finally {
|
|
177
|
+
setBusy(false);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const onRevert = async () => {
|
|
182
|
+
if (!window.confirm('Discard all in-progress edits? This cannot be undone.')) return;
|
|
183
|
+
setBusy(true);
|
|
184
|
+
try {
|
|
185
|
+
await tis.revert();
|
|
186
|
+
} catch (e: any) {
|
|
187
|
+
setDraftError(String(e?.message ?? e));
|
|
188
|
+
} finally {
|
|
189
|
+
setBusy(false);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className="tis-editor">
|
|
195
|
+
<header className="tis-editor__header">
|
|
196
|
+
<h2>
|
|
197
|
+
Test Methods{' '}
|
|
198
|
+
{tis.config?.dirty && (
|
|
199
|
+
<span className="tis-editor__dirty-pill">unsaved</span>
|
|
200
|
+
)}
|
|
201
|
+
</h2>
|
|
202
|
+
<div className="tis-editor__header-actions">
|
|
203
|
+
<Button
|
|
204
|
+
label="Save…"
|
|
205
|
+
icon="pi pi-save"
|
|
206
|
+
disabled={busy || !tis.config?.dirty}
|
|
207
|
+
onClick={openSaveDialog}
|
|
208
|
+
/>
|
|
209
|
+
<Button
|
|
210
|
+
label="Revert"
|
|
211
|
+
icon="pi pi-undo"
|
|
212
|
+
className="p-button-secondary"
|
|
213
|
+
disabled={busy || !tis.config?.dirty}
|
|
214
|
+
onClick={onRevert}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
</header>
|
|
218
|
+
|
|
219
|
+
{tis.error && (
|
|
220
|
+
<div className="tis-editor__error">
|
|
221
|
+
<strong>Error:</strong> <pre>{tis.error}</pre>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
<div className="tis-editor__body">
|
|
226
|
+
<aside className="tis-editor__sidebar">
|
|
227
|
+
<div className="tis-editor__sidebar-actions">
|
|
228
|
+
<Button
|
|
229
|
+
label="New"
|
|
230
|
+
icon="pi pi-plus"
|
|
231
|
+
disabled={busy}
|
|
232
|
+
onClick={() => setNewDialogOpen(true)}
|
|
233
|
+
/>
|
|
234
|
+
<Button
|
|
235
|
+
label="Duplicate"
|
|
236
|
+
icon="pi pi-clone"
|
|
237
|
+
className="p-button-secondary"
|
|
238
|
+
disabled={busy || !selectedId}
|
|
239
|
+
onClick={onDuplicate}
|
|
240
|
+
/>
|
|
241
|
+
<Button
|
|
242
|
+
label="Delete"
|
|
243
|
+
icon="pi pi-trash"
|
|
244
|
+
className="p-button-danger"
|
|
245
|
+
disabled={busy || !selectedId}
|
|
246
|
+
onClick={onDelete}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
<DataTable
|
|
250
|
+
value={rows}
|
|
251
|
+
selection={rows.find(r => r.id === selectedId) ?? null}
|
|
252
|
+
onSelectionChange={(e) => setSelectedId((e.value as MethodRow | null)?.id ?? null)}
|
|
253
|
+
selectionMode="single"
|
|
254
|
+
dataKey="id"
|
|
255
|
+
scrollable
|
|
256
|
+
scrollHeight="flex"
|
|
257
|
+
emptyMessage={tis.loading ? 'Loading…' : 'No test methods defined.'}
|
|
258
|
+
>
|
|
259
|
+
<Column field="id" header="Method ID" />
|
|
260
|
+
<Column field="label" header="Label" />
|
|
261
|
+
</DataTable>
|
|
262
|
+
</aside>
|
|
263
|
+
|
|
264
|
+
<section className="tis-editor__detail">
|
|
265
|
+
{selectedId && tis.config?.methods[selectedId] ? (
|
|
266
|
+
<>
|
|
267
|
+
{draftError && (
|
|
268
|
+
<div className="tis-editor__error">
|
|
269
|
+
<pre>{draftError}</pre>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
<MethodFormEditor
|
|
273
|
+
methodId={selectedId}
|
|
274
|
+
method={tis.config.methods[selectedId] as TestMethod}
|
|
275
|
+
onApply={onApply}
|
|
276
|
+
busy={busy}
|
|
277
|
+
knownAssetTypes={ams.types}
|
|
278
|
+
/>
|
|
279
|
+
</>
|
|
280
|
+
) : (
|
|
281
|
+
<div className="tis-editor__empty">
|
|
282
|
+
Select a test method on the left, or create a new one.
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
</section>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<Dialog
|
|
289
|
+
header="New Test Method"
|
|
290
|
+
visible={newDialogOpen}
|
|
291
|
+
onHide={() => setNewDialogOpen(false)}
|
|
292
|
+
style={{ width: '24rem' }}
|
|
293
|
+
>
|
|
294
|
+
<label className="tis-editor__new-method-label">
|
|
295
|
+
Method ID
|
|
296
|
+
<InputText
|
|
297
|
+
value={newId}
|
|
298
|
+
onChange={(e) => setNewId(e.target.value)}
|
|
299
|
+
placeholder="e.g. translational_traction"
|
|
300
|
+
autoFocus
|
|
301
|
+
/>
|
|
302
|
+
</label>
|
|
303
|
+
<small>Canonical key — appears in wire payloads, on-disk paths, and generated code.</small>
|
|
304
|
+
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end', marginTop: '1rem' }}>
|
|
305
|
+
<Button label="Cancel" className="p-button-text" onClick={() => setNewDialogOpen(false)} />
|
|
306
|
+
<Button label="Create" disabled={!newId.trim() || busy} onClick={onCreate} />
|
|
307
|
+
</div>
|
|
308
|
+
</Dialog>
|
|
309
|
+
|
|
310
|
+
<SaveDiffDialog
|
|
311
|
+
visible={saveDialogOpen}
|
|
312
|
+
staged={(tis.config?.methods ?? {}) as Record<string, TestMethod>}
|
|
313
|
+
invoker={effectiveInvoker}
|
|
314
|
+
onConfirm={onSaveConfirm}
|
|
315
|
+
onCancel={() => setSaveDialogOpen(false)}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export default TisConfigEditor;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { InputText } from 'primereact/inputtext';
|
|
2
|
+
import { Checkbox } from 'primereact/checkbox';
|
|
3
|
+
import { FormSection } from '../../forms/FormSection';
|
|
4
|
+
import { FormRow } from '../../forms/FormRow';
|
|
5
|
+
import type { AnalysisShape, TestMethod } from '../types';
|
|
6
|
+
|
|
7
|
+
export interface AnalysisEditorProps {
|
|
8
|
+
method: TestMethod;
|
|
9
|
+
onChange: (next: TestMethod) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const empty = (): AnalysisShape => ({ script: '', function: '' });
|
|
13
|
+
|
|
14
|
+
export const AnalysisEditor: React.FC<AnalysisEditorProps> = ({ method, onChange }) => {
|
|
15
|
+
const a = method.analysis as AnalysisShape | null | undefined;
|
|
16
|
+
const enabled = !!a;
|
|
17
|
+
|
|
18
|
+
const setAnalysis = (next: AnalysisShape | null) => {
|
|
19
|
+
onChange({ ...method, analysis: next });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<FormSection
|
|
24
|
+
title="Analysis Hook"
|
|
25
|
+
description="Optional post-cycle Python entry point. Codegen wires this into the method's TestManager."
|
|
26
|
+
actions={
|
|
27
|
+
<Checkbox
|
|
28
|
+
checked={enabled}
|
|
29
|
+
onChange={(e) => setAnalysis(e.checked ? empty() : null)}
|
|
30
|
+
/>
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
{!enabled && <small>Analysis hook disabled. Tick the box to enable.</small>}
|
|
34
|
+
{enabled && a && (
|
|
35
|
+
<>
|
|
36
|
+
<FormRow label="Script" required hint="Path under autocore-python's scripts directory.">
|
|
37
|
+
<InputText
|
|
38
|
+
value={a.script}
|
|
39
|
+
onChange={(e) => setAnalysis({ ...a, script: e.target.value })}
|
|
40
|
+
placeholder="e.g. analysis/traction_v1.py"
|
|
41
|
+
/>
|
|
42
|
+
</FormRow>
|
|
43
|
+
<FormRow label="Function" required hint="Entry-point name within the script.">
|
|
44
|
+
<InputText
|
|
45
|
+
value={a.function}
|
|
46
|
+
onChange={(e) => setAnalysis({ ...a, function: e.target.value })}
|
|
47
|
+
placeholder="e.g. analyze_cycle"
|
|
48
|
+
/>
|
|
49
|
+
</FormRow>
|
|
50
|
+
</>
|
|
51
|
+
)}
|
|
52
|
+
</FormSection>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -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
|
+
};
|