@adcops/autocore-react 3.3.50 → 3.3.57
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/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/ResultHistoryTable.d.ts +14 -2
- package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/TestDataView.d.ts +9 -5
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestRawDataView.d.ts +9 -5
- package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
- package/dist/components/tis/TestRawDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +15 -4
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +93 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -0
- package/dist/components/tis/TisProvider.js +1 -0
- package/dist/hub/HubWebSocket.d.ts +13 -0
- package/dist/hub/HubWebSocket.d.ts.map +1 -1
- package/dist/hub/HubWebSocket.js +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +22 -1
- package/src/components/tis/ResultHistoryTable.tsx +133 -48
- package/src/components/tis/TestDataView.tsx +70 -36
- package/src/components/tis/TestRawDataView.tsx +39 -22
- package/src/components/tis/TestSetupForm.tsx +155 -96
- package/src/components/tis/TisProvider.tsx +405 -0
- package/src/hub/HubWebSocket.ts +66 -3
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import React, { useState, useEffect, useContext } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
2
2
|
import { AutoComplete } from 'primereact/autocomplete';
|
|
3
3
|
import type { AutoCompleteCompleteEvent } from 'primereact/autocomplete';
|
|
4
|
+
import { SelectButton } from 'primereact/selectbutton';
|
|
4
5
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
5
6
|
import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
|
|
6
7
|
import { MessageType } from '../../hub/CommandMessage';
|
|
7
8
|
import { ValueInput } from '../ValueInput';
|
|
8
9
|
import { TextInput } from '../TextInput';
|
|
9
|
-
import {
|
|
10
|
+
import { useTis } from './TisProvider';
|
|
10
11
|
|
|
11
12
|
export interface TestFieldDef {
|
|
12
13
|
name: string;
|
|
@@ -16,84 +17,118 @@ export interface TestFieldDef {
|
|
|
16
17
|
source?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
export interface
|
|
20
|
+
export interface TestMethod {
|
|
20
21
|
project_fields: TestFieldDef[];
|
|
21
22
|
config_fields: TestFieldDef[];
|
|
22
23
|
cycle_fields: TestFieldDef[];
|
|
23
24
|
results_fields: TestFieldDef[];
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Props are all optional overrides — by default the form drives itself
|
|
29
|
+
* from the surrounding `<TisProvider>`. Pass any of these to lock that
|
|
30
|
+
* particular axis.
|
|
31
|
+
*
|
|
32
|
+
* - `schema`: bypass `useTisSchemas()` for the selected method.
|
|
33
|
+
* - `defaultProjectId` / `defaultMethodId`: seed the form's local
|
|
34
|
+
* state when the corresponding TIS-context fields are blank.
|
|
35
|
+
* - `onProjectChange` / `onMethodChange` / `onValidationChange`:
|
|
36
|
+
* receive callbacks in addition to the provider's selection state.
|
|
37
|
+
*/
|
|
26
38
|
export interface TestSetupFormProps {
|
|
27
|
-
schema
|
|
39
|
+
schema?: TestMethod;
|
|
28
40
|
defaultProjectId?: string;
|
|
29
|
-
|
|
41
|
+
defaultMethodId?: string;
|
|
30
42
|
onProjectChange?: (projectId: string) => void;
|
|
31
|
-
|
|
43
|
+
onMethodChange?: (methodId: string) => void;
|
|
32
44
|
onValidationChange?: (isValid: boolean, config: any) => void;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
36
|
-
schema,
|
|
37
|
-
defaultProjectId
|
|
38
|
-
|
|
47
|
+
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
48
|
+
schema: schemaOverride,
|
|
49
|
+
defaultProjectId,
|
|
50
|
+
defaultMethodId,
|
|
39
51
|
onProjectChange,
|
|
40
|
-
|
|
41
|
-
onValidationChange
|
|
52
|
+
onMethodChange,
|
|
53
|
+
onValidationChange,
|
|
42
54
|
}) => {
|
|
55
|
+
const tis = useTis();
|
|
56
|
+
const { invoke, write } = useContext(EventEmitterContext);
|
|
57
|
+
const { rawValues, findTagByFqdn } = useContext(AutoCoreTagContext);
|
|
58
|
+
|
|
59
|
+
const methodIds = useMemo(() => Object.keys(tis.schemas), [tis.schemas]);
|
|
60
|
+
|
|
61
|
+
// Seed the form's local project/method state from the provider's
|
|
62
|
+
// selection. Direct edits update the provider via setSelection so
|
|
63
|
+
// the History tab and Data tab follow along.
|
|
64
|
+
const [projectId, setProjectIdLocal] = useState<string>(
|
|
65
|
+
tis.selection.projectId || defaultProjectId || ''
|
|
66
|
+
);
|
|
67
|
+
const [methodId, setMethodIdLocal] = useState<string>(
|
|
68
|
+
tis.selection.methodId || defaultMethodId || tis.defaultMethodId || ''
|
|
69
|
+
);
|
|
70
|
+
const [sampleId, setSampleIdLocal] = useState<string>('');
|
|
43
71
|
const [config, setConfig] = useState<any>({});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
|
|
73
|
+
// Resolve the schema for the active method. The override beats the
|
|
74
|
+
// registry; if neither is available, render the empty state.
|
|
75
|
+
const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
|
|
76
|
+
|
|
77
|
+
// Push local edits back into the provider's selection so other
|
|
78
|
+
// components react. Only push when the value actually differs to
|
|
79
|
+
// avoid feedback loops.
|
|
48
80
|
useEffect(() => {
|
|
81
|
+
if (tis.selection.projectId !== projectId) {
|
|
82
|
+
tis.setSelection({ projectId });
|
|
83
|
+
}
|
|
49
84
|
if (onProjectChange) onProjectChange(projectId);
|
|
50
|
-
|
|
85
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
+
}, [projectId]);
|
|
51
87
|
|
|
52
88
|
useEffect(() => {
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
89
|
+
if (tis.selection.methodId !== methodId && methodId) {
|
|
90
|
+
tis.setSelection({ methodId });
|
|
91
|
+
}
|
|
92
|
+
if (onMethodChange) onMethodChange(methodId);
|
|
93
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
94
|
+
}, [methodId]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (tis.selection.sampleId !== sampleId) {
|
|
98
|
+
tis.setSelection({ sampleId });
|
|
99
|
+
}
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
}, [sampleId]);
|
|
102
|
+
|
|
103
|
+
// Track the live broadcast scalars — when the form is staged for
|
|
104
|
+
// a different sample externally (e.g., the operator types in
|
|
105
|
+
// another tab), reflect that here. Only react on a true change
|
|
106
|
+
// to avoid stomping local edits.
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (tis.state.stagedSampleId && tis.state.stagedSampleId !== sampleId) {
|
|
109
|
+
setSampleIdLocal(tis.state.stagedSampleId);
|
|
110
|
+
}
|
|
111
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
112
|
+
}, [tis.state.stagedSampleId]);
|
|
113
|
+
|
|
56
114
|
const [existingProjects, setExistingProjects] = useState<string[]>([]);
|
|
57
115
|
const [filteredProjects, setFilteredProjects] = useState<string[]>([]);
|
|
58
|
-
|
|
59
116
|
const [isValid, setIsValid] = useState(false);
|
|
60
|
-
const { invoke, write } = useContext(EventEmitterContext);
|
|
61
|
-
const { rawValues, findTagByFqdn } = useContext(AutoCoreTagContext);
|
|
62
117
|
|
|
63
|
-
// Seed and live-update fields that declare a `source
|
|
64
|
-
//
|
|
65
|
-
// The schema's `source` is the remote FQDN (e.g. "gm.x_cycle_move_speed"),
|
|
66
|
-
// but the event bus dispatches on the local `tagName` that the
|
|
67
|
-
// AutoCoreTagProvider registered. We resolve FQDN → tagName via
|
|
68
|
-
// `findTagByFqdn` and then read straight from the provider's `rawValues`,
|
|
69
|
-
// which the provider already eager-reads on mount and keeps current via
|
|
70
|
-
// subscriptions. That single source covers both the initial population
|
|
71
|
-
// (page reload / definition change) and live updates — no separate
|
|
72
|
-
// subscribe or invoke needed here.
|
|
118
|
+
// Seed and live-update fields that declare a `source` (FQDN).
|
|
73
119
|
useEffect(() => {
|
|
74
120
|
if (!schema) return;
|
|
75
|
-
|
|
76
121
|
const allFields = [...schema.project_fields, ...schema.config_fields];
|
|
77
|
-
|
|
78
122
|
setConfig((prev: any) => {
|
|
79
123
|
let next = prev;
|
|
80
124
|
for (const field of allFields) {
|
|
125
|
+
if (field.name === 'sample_id') continue; // sample_id is top-level now
|
|
81
126
|
if (!field.source) continue;
|
|
82
|
-
|
|
83
127
|
const tag = findTagByFqdn(field.source);
|
|
84
|
-
if (!tag)
|
|
85
|
-
console.warn(
|
|
86
|
-
`TestSetupForm: no tag registered for source "${field.source}" ` +
|
|
87
|
-
`(field "${field.name}"). Add it to acTagSpec with "ux": true ` +
|
|
88
|
-
`in project.json, or this field won't populate or update.`
|
|
89
|
-
);
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
128
|
+
if (!tag) continue;
|
|
93
129
|
const rawVal = rawValues[tag.tagName];
|
|
94
130
|
if (rawVal === undefined || rawVal === null) continue;
|
|
95
131
|
if (next[field.name] === rawVal) continue;
|
|
96
|
-
|
|
97
132
|
if (next === prev) next = { ...prev };
|
|
98
133
|
next[field.name] = rawVal;
|
|
99
134
|
}
|
|
@@ -104,12 +139,12 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
104
139
|
useEffect(() => {
|
|
105
140
|
const fetchProjects = async () => {
|
|
106
141
|
try {
|
|
107
|
-
const resp: any = await invoke('
|
|
142
|
+
const resp: any = await invoke('tis.list_projects' as any, MessageType.Request as any, {} as any);
|
|
108
143
|
if (resp.success && resp.data && resp.data.projects) {
|
|
109
144
|
setExistingProjects(resp.data.projects);
|
|
110
145
|
}
|
|
111
146
|
} catch (err) {
|
|
112
|
-
console.error(
|
|
147
|
+
console.error('Failed to list projects', err);
|
|
113
148
|
}
|
|
114
149
|
};
|
|
115
150
|
fetchProjects();
|
|
@@ -120,66 +155,77 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
120
155
|
setFilteredProjects(existingProjects.filter(p => p.toLowerCase().includes(query)));
|
|
121
156
|
};
|
|
122
157
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
// Validation drives both the local UI and the auto-stage. We only
|
|
159
|
+
// call `tis.stage_test` when every required piece is present —
|
|
160
|
+
// otherwise the operator's edge-triggered Start button could fire
|
|
161
|
+
// a half-baked stage.
|
|
128
162
|
useEffect(() => {
|
|
129
|
-
if (!schema) return;
|
|
163
|
+
if (!schema) { setIsValid(false); return; }
|
|
130
164
|
let valid = true;
|
|
131
|
-
if (!projectId
|
|
132
|
-
if (!
|
|
133
|
-
|
|
165
|
+
if (!projectId.trim()) valid = false;
|
|
166
|
+
if (!methodId.trim()) valid = false;
|
|
167
|
+
if (!sampleId.trim()) valid = false;
|
|
168
|
+
|
|
134
169
|
const allFields = [...schema.project_fields, ...schema.config_fields];
|
|
135
|
-
|
|
136
170
|
for (const field of allFields) {
|
|
171
|
+
if (field.name === 'sample_id') continue; // tracked separately
|
|
137
172
|
if (field.required) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
173
|
+
const v = config[field.name];
|
|
174
|
+
if (v === undefined || v === '' || v === null) { valid = false; break; }
|
|
142
175
|
}
|
|
143
176
|
}
|
|
144
177
|
setIsValid(valid);
|
|
145
|
-
if (onValidationChange)
|
|
146
|
-
|
|
178
|
+
if (onValidationChange) onValidationChange(valid, config);
|
|
179
|
+
|
|
180
|
+
// Auto-stage when everything is valid. Stripping sample_id from
|
|
181
|
+
// config (it's a top-level peer of project_id/method_id now;
|
|
182
|
+
// the legacy nested form is server-side hoisted with a
|
|
183
|
+
// deprecation warning, but new code shouldn't rely on it).
|
|
184
|
+
if (valid) {
|
|
185
|
+
const { sample_id: _drop, ...rest } = (config ?? {}) as any;
|
|
186
|
+
void invoke('tis.stage_test' as any, MessageType.Request, {
|
|
187
|
+
project_id: projectId,
|
|
188
|
+
method_id: methodId,
|
|
189
|
+
sample_id: sampleId,
|
|
190
|
+
config: rest,
|
|
191
|
+
} as any).catch(e => console.error('[TestSetupForm] stage_test failed:', e));
|
|
147
192
|
}
|
|
148
|
-
}, [config, schema, projectId,
|
|
193
|
+
}, [config, schema, projectId, methodId, sampleId, onValidationChange, invoke]);
|
|
149
194
|
|
|
150
195
|
const isFieldValid = (field: TestFieldDef) => {
|
|
151
196
|
if (!field.required) return true;
|
|
152
|
-
|
|
197
|
+
const v = config[field.name];
|
|
198
|
+
return v !== undefined && v !== '' && v !== null;
|
|
153
199
|
};
|
|
154
200
|
|
|
155
201
|
const handleProjectIdChange = (value: string | null | undefined) => {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
const sanitized = (value || '').replace(/[^a-zA-Z0-9_]/g, '');
|
|
203
|
+
setProjectIdLocal(sanitized);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleSampleIdChange = (value: string) => {
|
|
207
|
+
setSampleIdLocal(value);
|
|
159
208
|
};
|
|
160
209
|
|
|
161
210
|
const handleFieldChange = async (field: TestFieldDef, val: any) => {
|
|
162
|
-
setConfig({...config, [field.name]: val});
|
|
211
|
+
setConfig({ ...config, [field.name]: val });
|
|
163
212
|
if (field.source) {
|
|
164
|
-
try {
|
|
165
|
-
|
|
166
|
-
} catch(e) {
|
|
167
|
-
console.error("Failed to write to source:", e);
|
|
168
|
-
}
|
|
213
|
+
try { await write(field.source, val); }
|
|
214
|
+
catch (e) { console.error('Failed to write to source:', e); }
|
|
169
215
|
}
|
|
170
216
|
};
|
|
171
217
|
|
|
172
218
|
const renderField = (field: TestFieldDef) => {
|
|
219
|
+
if (field.name === 'sample_id') return null;
|
|
173
220
|
const valid = isFieldValid(field);
|
|
174
221
|
const isNum = field.type !== 'string' && field.type !== 'bool';
|
|
175
|
-
|
|
176
222
|
return (
|
|
177
223
|
<React.Fragment key={field.name}>
|
|
178
224
|
<span className="ac-form-label">{field.name}</span>
|
|
179
225
|
{isNum ? (
|
|
180
226
|
<ValueInput
|
|
181
227
|
label={undefined}
|
|
182
|
-
value={config[field.name] != null ? Number(config[field.name]) : null}
|
|
228
|
+
value={config[field.name] != null ? Number(config[field.name]) : null}
|
|
183
229
|
onValueChanged={(val) => handleFieldChange(field, val)}
|
|
184
230
|
className={!valid ? 'p-invalid' : ''}
|
|
185
231
|
/>
|
|
@@ -191,11 +237,9 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
191
237
|
className={!valid ? 'p-invalid' : ''}
|
|
192
238
|
/>
|
|
193
239
|
)}
|
|
194
|
-
|
|
195
240
|
<span className="ac-form-units">{field.units ?? ''}</span>
|
|
196
|
-
|
|
197
241
|
<span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
198
|
-
<i className={valid ?
|
|
242
|
+
<i className={valid ? 'pi pi-check' : 'pi pi-times'} />
|
|
199
243
|
</span>
|
|
200
244
|
</React.Fragment>
|
|
201
245
|
);
|
|
@@ -204,7 +248,9 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
204
248
|
if (!schema) {
|
|
205
249
|
return (
|
|
206
250
|
<div className="ac-form-grid" style={{ padding: '1.25rem' }}>
|
|
207
|
-
<h3 className="ac-form-section">
|
|
251
|
+
<h3 className="ac-form-section">
|
|
252
|
+
{tis.schemasLoaded ? 'No Test Method Selected' : 'Loading test methods…'}
|
|
253
|
+
</h3>
|
|
208
254
|
</div>
|
|
209
255
|
);
|
|
210
256
|
}
|
|
@@ -212,12 +258,12 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
212
258
|
return (
|
|
213
259
|
<div className="ac-form-grid" style={{ padding: '1.25rem' }}>
|
|
214
260
|
<h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
215
|
-
Project &
|
|
261
|
+
Project & Method
|
|
216
262
|
<span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
217
|
-
<i className={isValid ?
|
|
263
|
+
<i className={isValid ? 'pi pi-check-circle' : 'pi pi-exclamation-circle'} />
|
|
218
264
|
</span>
|
|
219
265
|
</h3>
|
|
220
|
-
|
|
266
|
+
|
|
221
267
|
<span className="ac-form-label">Project ID</span>
|
|
222
268
|
<AutoComplete
|
|
223
269
|
value={projectId}
|
|
@@ -226,28 +272,41 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
|
226
272
|
onChange={(e) => handleProjectIdChange(e.value)}
|
|
227
273
|
dropdown
|
|
228
274
|
placeholder="Enter or select Project ID"
|
|
229
|
-
className={!projectId
|
|
275
|
+
className={!projectId.trim() ? 'p-invalid' : ''}
|
|
230
276
|
/>
|
|
231
277
|
<span />
|
|
232
|
-
<span style={{ color: projectId
|
|
233
|
-
<i className={projectId
|
|
278
|
+
<span style={{ color: projectId.trim() ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
279
|
+
<i className={projectId.trim() ? 'pi pi-check' : 'pi pi-times'} />
|
|
234
280
|
</span>
|
|
235
281
|
|
|
236
|
-
<span className="ac-form-label">
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
className={!
|
|
282
|
+
<span className="ac-form-label">Sample ID</span>
|
|
283
|
+
<TextInput
|
|
284
|
+
label={undefined}
|
|
285
|
+
value={sampleId}
|
|
286
|
+
onValueChanged={handleSampleIdChange}
|
|
287
|
+
className={!sampleId.trim() ? 'p-invalid' : ''}
|
|
242
288
|
/>
|
|
243
289
|
<span />
|
|
244
|
-
<span style={{ color:
|
|
245
|
-
<i className={
|
|
290
|
+
<span style={{ color: sampleId.trim() ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
291
|
+
<i className={sampleId.trim() ? 'pi pi-check' : 'pi pi-times'} />
|
|
246
292
|
</span>
|
|
247
293
|
|
|
294
|
+
{methodIds.length > 1 && (
|
|
295
|
+
<>
|
|
296
|
+
<span className="ac-form-label">Test Method</span>
|
|
297
|
+
<SelectButton
|
|
298
|
+
value={methodId}
|
|
299
|
+
options={methodIds.map(id => ({ label: id, value: id }))}
|
|
300
|
+
onChange={(e) => e.value && setMethodIdLocal(e.value)}
|
|
301
|
+
/>
|
|
302
|
+
<span />
|
|
303
|
+
<span />
|
|
304
|
+
</>
|
|
305
|
+
)}
|
|
306
|
+
|
|
248
307
|
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Project Information</h3>
|
|
249
308
|
{schema.project_fields.map(renderField)}
|
|
250
|
-
|
|
309
|
+
|
|
251
310
|
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
|
|
252
311
|
{schema.config_fields.map(renderField)}
|
|
253
312
|
</div>
|