@adcops/autocore-react 3.3.48 → 3.3.54
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 +10 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/ResultHistoryTable.d.ts +19 -0
- package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -0
- package/dist/components/tis/ResultHistoryTable.js +1 -0
- package/dist/components/{TestDataView.d.ts → tis/TestDataView.d.ts} +9 -5
- package/dist/components/tis/TestDataView.d.ts.map +1 -0
- package/dist/components/tis/TestDataView.js +1 -0
- package/dist/components/tis/TestRawDataView.d.ts +18 -0
- package/dist/components/tis/TestRawDataView.d.ts.map +1 -0
- package/dist/components/tis/TestRawDataView.js +1 -0
- package/dist/components/tis/TestSetupForm.d.ts +35 -0
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -0
- package/dist/components/tis/TestSetupForm.js +1 -0
- 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/package.json +1 -1
- package/src/components/index.ts +40 -2
- package/src/components/tis/ResultHistoryTable.tsx +277 -0
- package/src/components/{TestDataView.tsx → tis/TestDataView.tsx} +72 -38
- package/src/components/{TestRawDataView.tsx → tis/TestRawDataView.tsx} +41 -24
- package/src/components/tis/TestSetupForm.tsx +314 -0
- package/src/components/tis/TisProvider.tsx +404 -0
- package/dist/components/ResultHistoryTable.d.ts +0 -7
- package/dist/components/ResultHistoryTable.d.ts.map +0 -1
- package/dist/components/ResultHistoryTable.js +0 -1
- package/dist/components/TestDataView.d.ts.map +0 -1
- package/dist/components/TestDataView.js +0 -1
- package/dist/components/TestRawDataView.d.ts +0 -14
- package/dist/components/TestRawDataView.d.ts.map +0 -1
- package/dist/components/TestRawDataView.js +0 -1
- package/dist/components/TestSetupForm.d.ts +0 -24
- package/dist/components/TestSetupForm.d.ts.map +0 -1
- package/dist/components/TestSetupForm.js +0 -1
- package/src/components/ResultHistoryTable.tsx +0 -162
- package/src/components/TestSetupForm.tsx +0 -255
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import React, { useState, useEffect, useContext, useMemo } from 'react';
|
|
2
|
+
import { AutoComplete } from 'primereact/autocomplete';
|
|
3
|
+
import type { AutoCompleteCompleteEvent } from 'primereact/autocomplete';
|
|
4
|
+
import { SelectButton } from 'primereact/selectbutton';
|
|
5
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
6
|
+
import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
|
|
7
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
8
|
+
import { ValueInput } from '../ValueInput';
|
|
9
|
+
import { TextInput } from '../TextInput';
|
|
10
|
+
import { useTis } from './TisProvider';
|
|
11
|
+
|
|
12
|
+
export interface TestFieldDef {
|
|
13
|
+
name: string;
|
|
14
|
+
type: string;
|
|
15
|
+
units?: string;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
source?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TestMethod {
|
|
21
|
+
project_fields: TestFieldDef[];
|
|
22
|
+
config_fields: TestFieldDef[];
|
|
23
|
+
cycle_fields: TestFieldDef[];
|
|
24
|
+
results_fields: TestFieldDef[];
|
|
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
|
+
*/
|
|
38
|
+
export interface TestSetupFormProps {
|
|
39
|
+
schema?: TestMethod;
|
|
40
|
+
defaultProjectId?: string;
|
|
41
|
+
defaultMethodId?: string;
|
|
42
|
+
onProjectChange?: (projectId: string) => void;
|
|
43
|
+
onMethodChange?: (methodId: string) => void;
|
|
44
|
+
onValidationChange?: (isValid: boolean, config: any) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
48
|
+
schema: schemaOverride,
|
|
49
|
+
defaultProjectId,
|
|
50
|
+
defaultMethodId,
|
|
51
|
+
onProjectChange,
|
|
52
|
+
onMethodChange,
|
|
53
|
+
onValidationChange,
|
|
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>('');
|
|
71
|
+
const [config, setConfig] = useState<any>({});
|
|
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.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (tis.selection.projectId !== projectId) {
|
|
82
|
+
tis.setSelection({ projectId });
|
|
83
|
+
}
|
|
84
|
+
if (onProjectChange) onProjectChange(projectId);
|
|
85
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
+
}, [projectId]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
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
|
+
|
|
114
|
+
const [existingProjects, setExistingProjects] = useState<string[]>([]);
|
|
115
|
+
const [filteredProjects, setFilteredProjects] = useState<string[]>([]);
|
|
116
|
+
const [isValid, setIsValid] = useState(false);
|
|
117
|
+
|
|
118
|
+
// Seed and live-update fields that declare a `source` (FQDN).
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (!schema) return;
|
|
121
|
+
const allFields = [...schema.project_fields, ...schema.config_fields];
|
|
122
|
+
setConfig((prev: any) => {
|
|
123
|
+
let next = prev;
|
|
124
|
+
for (const field of allFields) {
|
|
125
|
+
if (field.name === 'sample_id') continue; // sample_id is top-level now
|
|
126
|
+
if (!field.source) continue;
|
|
127
|
+
const tag = findTagByFqdn(field.source);
|
|
128
|
+
if (!tag) continue;
|
|
129
|
+
const rawVal = rawValues[tag.tagName];
|
|
130
|
+
if (rawVal === undefined || rawVal === null) continue;
|
|
131
|
+
if (next[field.name] === rawVal) continue;
|
|
132
|
+
if (next === prev) next = { ...prev };
|
|
133
|
+
next[field.name] = rawVal;
|
|
134
|
+
}
|
|
135
|
+
return next;
|
|
136
|
+
});
|
|
137
|
+
}, [schema, rawValues, findTagByFqdn]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const fetchProjects = async () => {
|
|
141
|
+
try {
|
|
142
|
+
const resp: any = await invoke('tis.list_projects' as any, MessageType.Request as any, {} as any);
|
|
143
|
+
if (resp.success && resp.data && resp.data.projects) {
|
|
144
|
+
setExistingProjects(resp.data.projects);
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error('Failed to list projects', err);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
fetchProjects();
|
|
151
|
+
}, [invoke]);
|
|
152
|
+
|
|
153
|
+
const searchProjects = (event: AutoCompleteCompleteEvent) => {
|
|
154
|
+
const query = event.query.toLowerCase();
|
|
155
|
+
setFilteredProjects(existingProjects.filter(p => p.toLowerCase().includes(query)));
|
|
156
|
+
};
|
|
157
|
+
|
|
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.
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!schema) { setIsValid(false); return; }
|
|
164
|
+
let valid = true;
|
|
165
|
+
if (!projectId.trim()) valid = false;
|
|
166
|
+
if (!methodId.trim()) valid = false;
|
|
167
|
+
if (!sampleId.trim()) valid = false;
|
|
168
|
+
|
|
169
|
+
const allFields = [...schema.project_fields, ...schema.config_fields];
|
|
170
|
+
for (const field of allFields) {
|
|
171
|
+
if (field.name === 'sample_id') continue; // tracked separately
|
|
172
|
+
if (field.required) {
|
|
173
|
+
const v = config[field.name];
|
|
174
|
+
if (v === undefined || v === '' || v === null) { valid = false; break; }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
setIsValid(valid);
|
|
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));
|
|
192
|
+
}
|
|
193
|
+
}, [config, schema, projectId, methodId, sampleId, onValidationChange, invoke]);
|
|
194
|
+
|
|
195
|
+
const isFieldValid = (field: TestFieldDef) => {
|
|
196
|
+
if (!field.required) return true;
|
|
197
|
+
const v = config[field.name];
|
|
198
|
+
return v !== undefined && v !== '' && v !== null;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const handleProjectIdChange = (value: string | null | undefined) => {
|
|
202
|
+
const sanitized = (value || '').replace(/[^a-zA-Z0-9_]/g, '');
|
|
203
|
+
setProjectIdLocal(sanitized);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleSampleIdChange = (value: string) => {
|
|
207
|
+
setSampleIdLocal(value);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const handleFieldChange = async (field: TestFieldDef, val: any) => {
|
|
211
|
+
setConfig({ ...config, [field.name]: val });
|
|
212
|
+
if (field.source) {
|
|
213
|
+
try { await write(field.source, val); }
|
|
214
|
+
catch (e) { console.error('Failed to write to source:', e); }
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const renderField = (field: TestFieldDef) => {
|
|
219
|
+
if (field.name === 'sample_id') return null;
|
|
220
|
+
const valid = isFieldValid(field);
|
|
221
|
+
const isNum = field.type !== 'string' && field.type !== 'bool';
|
|
222
|
+
return (
|
|
223
|
+
<React.Fragment key={field.name}>
|
|
224
|
+
<span className="ac-form-label">{field.name}</span>
|
|
225
|
+
{isNum ? (
|
|
226
|
+
<ValueInput
|
|
227
|
+
label={undefined}
|
|
228
|
+
value={config[field.name] != null ? Number(config[field.name]) : null}
|
|
229
|
+
onValueChanged={(val) => handleFieldChange(field, val)}
|
|
230
|
+
className={!valid ? 'p-invalid' : ''}
|
|
231
|
+
/>
|
|
232
|
+
) : (
|
|
233
|
+
<TextInput
|
|
234
|
+
label={undefined}
|
|
235
|
+
value={config[field.name] != null ? String(config[field.name]) : ''}
|
|
236
|
+
onValueChanged={(val) => handleFieldChange(field, val)}
|
|
237
|
+
className={!valid ? 'p-invalid' : ''}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
<span className="ac-form-units">{field.units ?? ''}</span>
|
|
241
|
+
<span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
|
|
242
|
+
<i className={valid ? 'pi pi-check' : 'pi pi-times'} />
|
|
243
|
+
</span>
|
|
244
|
+
</React.Fragment>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (!schema) {
|
|
249
|
+
return (
|
|
250
|
+
<div className="ac-form-grid" style={{ padding: '1.25rem' }}>
|
|
251
|
+
<h3 className="ac-form-section">
|
|
252
|
+
{tis.schemasLoaded ? 'No Test Method Selected' : 'Loading test methods…'}
|
|
253
|
+
</h3>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div className="ac-form-grid" style={{ padding: '1.25rem' }}>
|
|
260
|
+
<h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
261
|
+
Project & Method
|
|
262
|
+
<span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
263
|
+
<i className={isValid ? 'pi pi-check-circle' : 'pi pi-exclamation-circle'} />
|
|
264
|
+
</span>
|
|
265
|
+
</h3>
|
|
266
|
+
|
|
267
|
+
<span className="ac-form-label">Project ID</span>
|
|
268
|
+
<AutoComplete
|
|
269
|
+
value={projectId}
|
|
270
|
+
suggestions={filteredProjects}
|
|
271
|
+
completeMethod={searchProjects}
|
|
272
|
+
onChange={(e) => handleProjectIdChange(e.value)}
|
|
273
|
+
dropdown
|
|
274
|
+
placeholder="Enter or select Project ID"
|
|
275
|
+
className={!projectId.trim() ? 'p-invalid' : ''}
|
|
276
|
+
/>
|
|
277
|
+
<span />
|
|
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'} />
|
|
280
|
+
</span>
|
|
281
|
+
|
|
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' : ''}
|
|
288
|
+
/>
|
|
289
|
+
<span />
|
|
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'} />
|
|
292
|
+
</span>
|
|
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
|
+
|
|
307
|
+
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Project Information</h3>
|
|
308
|
+
{schema.project_fields.map(renderField)}
|
|
309
|
+
|
|
310
|
+
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
|
|
311
|
+
{schema.config_fields.map(renderField)}
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
};
|