@adcops/autocore-react 3.3.75 → 3.3.79
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/Indicator.d.ts +29 -52
- package/dist/components/Indicator.d.ts.map +1 -1
- package/dist/components/Indicator.js +1 -1
- package/dist/components/ams/AmsProvider.d.ts +7 -0
- package/dist/components/ams/AmsProvider.d.ts.map +1 -1
- package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
- package/dist/components/ams/AssetDetailView.js +1 -1
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
- package/dist/components/ams/AssetRegistryTable.js +1 -1
- package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -1
- package/dist/components/ams/CalibrationEntryDialog.js +1 -1
- package/dist/components/ams/MissingAssetsBanner.d.ts +11 -0
- package/dist/components/ams/MissingAssetsBanner.d.ts.map +1 -0
- package/dist/components/ams/MissingAssetsBanner.js +1 -0
- package/dist/components/ams/PlaceholderHealthPanel.d.ts +3 -0
- package/dist/components/ams/PlaceholderHealthPanel.d.ts.map +1 -0
- package/dist/components/ams/PlaceholderHealthPanel.js +1 -0
- package/dist/components/ams/index.d.ts +2 -0
- package/dist/components/ams/index.d.ts.map +1 -1
- package/dist/components/ams/index.js +1 -1
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/network/NetworkPanel.d.ts +8 -0
- package/dist/components/network/NetworkPanel.d.ts.map +1 -0
- package/dist/components/network/NetworkPanel.js +1 -0
- package/dist/components/network/NetworkProvider.d.ts +72 -0
- package/dist/components/network/NetworkProvider.d.ts.map +1 -0
- package/dist/components/network/NetworkProvider.js +1 -0
- package/dist/components/network/StagedChangeBanner.d.ts +8 -0
- package/dist/components/network/StagedChangeBanner.d.ts.map +1 -0
- package/dist/components/network/StagedChangeBanner.js +1 -0
- package/dist/components/network/index.d.ts +7 -0
- package/dist/components/network/index.d.ts.map +1 -0
- package/dist/components/network/index.js +1 -0
- package/dist/components/tis/ProjectManager.d.ts +7 -0
- package/dist/components/tis/ProjectManager.d.ts.map +1 -0
- package/dist/components/tis/ProjectManager.js +1 -0
- package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/package.json +1 -1
- package/src/components/Indicator.tsx +177 -162
- package/src/components/ams/AmsProvider.tsx +7 -0
- package/src/components/ams/AssetDetailView.tsx +287 -4
- package/src/components/ams/AssetRegistryTable.tsx +325 -21
- package/src/components/ams/CalibrationEntryDialog.tsx +163 -30
- package/src/components/ams/MissingAssetsBanner.tsx +124 -0
- package/src/components/ams/PlaceholderHealthPanel.tsx +188 -0
- package/src/components/ams/index.ts +2 -0
- package/src/components/index.ts +26 -0
- package/src/components/network/NetworkPanel.tsx +363 -0
- package/src/components/network/NetworkProvider.tsx +349 -0
- package/src/components/network/StagedChangeBanner.tsx +101 -0
- package/src/components/network/index.ts +17 -0
- package/src/components/tis/ProjectManager.tsx +393 -0
- package/src/components/tis/ResultHistoryTable.tsx +126 -188
|
@@ -6,19 +6,97 @@
|
|
|
6
6
|
* <CalibrationEntryDialog>.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useEffect, useState } from 'react';
|
|
9
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
10
10
|
import { Button } from 'primereact/button';
|
|
11
11
|
import { DataTable } from 'primereact/datatable';
|
|
12
12
|
import { Column } from 'primereact/column';
|
|
13
|
+
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
|
|
14
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
15
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
13
16
|
import { useAms } from './AmsProvider';
|
|
14
17
|
import { CalibrationEntryDialog } from './CalibrationEntryDialog';
|
|
15
18
|
|
|
16
19
|
export const AssetDetailView: React.FC = () => {
|
|
17
|
-
const { selection, schemas, roles, readAsset, listCalibrations, readCalibration, readUsage
|
|
20
|
+
const { selection, schemas, roles, readAsset, listCalibrations, readCalibration, readUsage,
|
|
21
|
+
refreshAssets, setSelection } = useAms();
|
|
22
|
+
const { invoke } = useContext(EventEmitterContext);
|
|
18
23
|
const [asset, setAsset] = useState<any | null>(null);
|
|
19
24
|
const [cals, setCals] = useState<any[]>([]);
|
|
20
25
|
const [usage, setUsage] = useState<any | null>(null);
|
|
21
26
|
const [calDialogOpen, setCalDialogOpen] = useState(false);
|
|
27
|
+
const [deleting, setDeleting] = useState(false);
|
|
28
|
+
|
|
29
|
+
/** Two-step asset retirement: flip status to retired via update_asset.
|
|
30
|
+
* The hard delete (delete_asset) is a separate action, only visible
|
|
31
|
+
* after this completes. Both go through ams.* IPC. */
|
|
32
|
+
const retire = async () => {
|
|
33
|
+
if (!asset) return;
|
|
34
|
+
try {
|
|
35
|
+
const resp: any = await invoke('ams.update_asset' as any, MessageType.Request, {
|
|
36
|
+
asset_id: asset.asset_id,
|
|
37
|
+
status: 'retired',
|
|
38
|
+
} as any);
|
|
39
|
+
if (resp?.success) {
|
|
40
|
+
await refreshAssets();
|
|
41
|
+
await refresh();
|
|
42
|
+
} else {
|
|
43
|
+
console.warn('[AssetDetailView] retire failed:', resp?.error_message);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error('[AssetDetailView] retire threw:', e);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Permanent delete. Removes the AMS record (asset.json + cal
|
|
51
|
+
* history + usage). Historical test records still reference this
|
|
52
|
+
* asset_id but carry the full asset+calibration snapshot inline
|
|
53
|
+
* so the audit trail survives the purge. */
|
|
54
|
+
const performDelete = async () => {
|
|
55
|
+
if (!asset) return;
|
|
56
|
+
setDeleting(true);
|
|
57
|
+
try {
|
|
58
|
+
const resp: any = await invoke('ams.delete_asset' as any, MessageType.Request, {
|
|
59
|
+
asset_id: asset.asset_id,
|
|
60
|
+
} as any);
|
|
61
|
+
if (resp?.success) {
|
|
62
|
+
setSelection({ assetType: null, assetId: null });
|
|
63
|
+
await refreshAssets();
|
|
64
|
+
} else {
|
|
65
|
+
console.warn('[AssetDetailView] delete failed:', resp?.error_message);
|
|
66
|
+
alert(`Delete failed: ${resp?.error_message ?? 'unknown error'}`);
|
|
67
|
+
}
|
|
68
|
+
} catch (e: any) {
|
|
69
|
+
console.error('[AssetDetailView] delete threw:', e);
|
|
70
|
+
alert(`Delete threw: ${e?.message ?? e}`);
|
|
71
|
+
} finally {
|
|
72
|
+
setDeleting(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const confirmDelete = () => {
|
|
77
|
+
if (!asset) return;
|
|
78
|
+
confirmDialog({
|
|
79
|
+
message: (
|
|
80
|
+
<div>
|
|
81
|
+
<p style={{ marginTop: 0 }}>
|
|
82
|
+
Permanently delete asset <code>{asset.asset_id}</code>?
|
|
83
|
+
</p>
|
|
84
|
+
<p style={{ marginBottom: 0, fontSize: '0.875rem', color: '#9ca3af' }}>
|
|
85
|
+
This removes the AMS record (nameplate, calibration history,
|
|
86
|
+
usage counters). Historical test records that referenced this
|
|
87
|
+
asset are unaffected — they carry the full asset and
|
|
88
|
+
calibration data inline.
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
header: 'Delete asset',
|
|
93
|
+
icon: 'pi pi-exclamation-triangle',
|
|
94
|
+
acceptLabel: 'Delete',
|
|
95
|
+
rejectLabel: 'Cancel',
|
|
96
|
+
acceptClassName: 'p-button-danger',
|
|
97
|
+
accept: () => { void performDelete(); },
|
|
98
|
+
});
|
|
99
|
+
};
|
|
22
100
|
|
|
23
101
|
const refresh = async () => {
|
|
24
102
|
if (!selection.assetId) {
|
|
@@ -56,6 +134,22 @@ export const AssetDetailView: React.FC = () => {
|
|
|
56
134
|
const roleHit = roles[asset.asset_type]?.find(r => r.location === asset.location);
|
|
57
135
|
const roleLabel = roleHit ? (roleHit.label ?? roleHit.location) : asset.location;
|
|
58
136
|
|
|
137
|
+
// Schema-declared nameplate fields (capacity_n, sensitivity_mv_v,
|
|
138
|
+
// etc.). Rendered in their own panel so operators can verify what
|
|
139
|
+
// the placeholder resolver will read for `${ams.by_location.*}`
|
|
140
|
+
// references at module-start time. Missing values render as a
|
|
141
|
+
// muted "(not set)" rather than empty space — that's the failure
|
|
142
|
+
// mode the resolver will block on, so we draw attention to it.
|
|
143
|
+
const schemaFields: any[] = Array.isArray(schemas[asset.asset_type]?.fields)
|
|
144
|
+
? schemas[asset.asset_type].fields
|
|
145
|
+
: [];
|
|
146
|
+
const custom = (asset.custom ?? {}) as Record<string, any>;
|
|
147
|
+
|
|
148
|
+
// Modules that resolve placeholders against this role. Drawn from
|
|
149
|
+
// ams.list_roles so the operator can see "this load cell feeds NI
|
|
150
|
+
// bridge channel calibration" at a glance.
|
|
151
|
+
const modulesUsingRole = roleHit?.used_by_modules ?? [];
|
|
152
|
+
|
|
59
153
|
return (
|
|
60
154
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
61
155
|
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr auto 1fr', gap: '0.5rem 1.5rem', alignItems: 'baseline' }}>
|
|
@@ -74,11 +168,76 @@ export const AssetDetailView: React.FC = () => {
|
|
|
74
168
|
<strong>Cycles</strong> <span>{usage?.cycles ?? 0}</span>
|
|
75
169
|
</div>
|
|
76
170
|
|
|
171
|
+
{modulesUsingRole.length > 0 && (
|
|
172
|
+
<div style={{ fontSize: '0.875rem', color: '#34d399' }}>
|
|
173
|
+
✓ Feeds module {modulesUsingRole.length === 1 ? 'config' : 'configs'}:{' '}
|
|
174
|
+
<strong>{modulesUsingRole.join(', ')}</strong>
|
|
175
|
+
{' — '}their <code>${'${ams.by_location.' + asset.location + '.*}'}</code>{' '}
|
|
176
|
+
placeholders will resolve to the values below.
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{schemaFields.length > 0 && (
|
|
181
|
+
<div>
|
|
182
|
+
<h4 style={{ margin: '0 0 0.5rem 0' }}>Nameplate</h4>
|
|
183
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr',
|
|
184
|
+
gap: '0.25rem 1rem', alignItems: 'baseline',
|
|
185
|
+
fontSize: '0.9rem' }}>
|
|
186
|
+
{schemaFields.map((f: any) => {
|
|
187
|
+
const v = custom[f.name];
|
|
188
|
+
const present = v !== undefined && v !== null && v !== '';
|
|
189
|
+
const label = f.label ?? f.name;
|
|
190
|
+
const display = present
|
|
191
|
+
? (f.units ? `${v} ${f.units}` : String(v))
|
|
192
|
+
: '(not set)';
|
|
193
|
+
return (
|
|
194
|
+
<React.Fragment key={f.name}>
|
|
195
|
+
<strong title={f.description ?? undefined}>{label}</strong>
|
|
196
|
+
<span style={{ color: present ? undefined : '#f59e0b' }}>
|
|
197
|
+
{display}
|
|
198
|
+
</span>
|
|
199
|
+
</React.Fragment>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
<SubLocationsPanel
|
|
207
|
+
schema={schemas[asset.asset_type]?.sub_locations}
|
|
208
|
+
values={asset.sub_locations}
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
<SubLocationsCalibrationPanel
|
|
212
|
+
schema={schemas[asset.asset_type]?.sub_locations}
|
|
213
|
+
currentCalibration={cals.find(c => c.cal_id === asset.current_calibration_id)}
|
|
214
|
+
/>
|
|
215
|
+
|
|
77
216
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
78
217
|
<h4 style={{ margin: 0 }}>Calibration History</h4>
|
|
79
|
-
<
|
|
80
|
-
|
|
218
|
+
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
219
|
+
{asset.status === 'active' && (
|
|
220
|
+
<Button label="Retire" icon="pi pi-power-off"
|
|
221
|
+
severity="warning" outlined
|
|
222
|
+
onClick={retire}
|
|
223
|
+
tooltip="Mark inactive. The asset stays on disk; deletion is a separate action available after retirement."
|
|
224
|
+
tooltipOptions={{ position: 'left' }}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
{asset.status === 'retired' && (
|
|
228
|
+
<Button label="Delete" icon="pi pi-trash"
|
|
229
|
+
severity="danger" outlined
|
|
230
|
+
loading={deleting}
|
|
231
|
+
onClick={confirmDelete}
|
|
232
|
+
tooltip="Permanent. Removes the AMS record; historical test snapshots are preserved inline."
|
|
233
|
+
tooltipOptions={{ position: 'left' }}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
236
|
+
<Button label="+ Calibration" icon="pi pi-plus"
|
|
237
|
+
onClick={() => setCalDialogOpen(true)} />
|
|
238
|
+
</div>
|
|
81
239
|
</div>
|
|
240
|
+
<ConfirmDialog />
|
|
82
241
|
<DataTable value={cals} size="small" stripedRows
|
|
83
242
|
emptyMessage="No calibrations recorded for this asset."
|
|
84
243
|
>
|
|
@@ -106,3 +265,127 @@ export const AssetDetailView: React.FC = () => {
|
|
|
106
265
|
</div>
|
|
107
266
|
);
|
|
108
267
|
};
|
|
268
|
+
|
|
269
|
+
// -------------------------------------------------------------------------
|
|
270
|
+
// Per-axis nameplate matrix. Renders when the asset's type declares a
|
|
271
|
+
// keyed-fields sub_locations schema (multi-axis transducers etc.).
|
|
272
|
+
// Read-only — registration / edits happen in the Add dialog.
|
|
273
|
+
// -------------------------------------------------------------------------
|
|
274
|
+
const SubLocationsPanel: React.FC<{ schema: any; values: any }> = ({ schema, values }) => {
|
|
275
|
+
if (!schema || !Array.isArray(schema.keys) || !Array.isArray(schema.fields)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const obj = (values && typeof values === 'object') ? values : {};
|
|
279
|
+
return (
|
|
280
|
+
<div>
|
|
281
|
+
<h4 style={{ margin: '0 0 0.5rem 0' }}>{schema.label ?? 'Sub-locations'}</h4>
|
|
282
|
+
<div style={{ overflowX: 'auto' }}>
|
|
283
|
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
|
284
|
+
<thead>
|
|
285
|
+
<tr>
|
|
286
|
+
<th style={{ textAlign: 'left', padding: '0.25rem 0.5rem',
|
|
287
|
+
borderBottom: '1px solid var(--surface-border)' }}>
|
|
288
|
+
{schema.key_label ?? 'Key'}
|
|
289
|
+
</th>
|
|
290
|
+
{schema.fields.map((f: any) => (
|
|
291
|
+
<th key={f.name}
|
|
292
|
+
title={f.description ?? undefined}
|
|
293
|
+
style={{ textAlign: 'left', padding: '0.25rem 0.5rem',
|
|
294
|
+
borderBottom: '1px solid var(--surface-border)' }}>
|
|
295
|
+
{(f.label ?? f.name)}{f.units ? ` [${f.units}]` : ''}
|
|
296
|
+
</th>
|
|
297
|
+
))}
|
|
298
|
+
</tr>
|
|
299
|
+
</thead>
|
|
300
|
+
<tbody>
|
|
301
|
+
{schema.keys.map((key: string) => {
|
|
302
|
+
const row = (obj[key] ?? {}) as Record<string, any>;
|
|
303
|
+
return (
|
|
304
|
+
<tr key={key}>
|
|
305
|
+
<td style={{ padding: '0.25rem 0.5rem', fontWeight: 600 }}>{key}</td>
|
|
306
|
+
{schema.fields.map((f: any) => {
|
|
307
|
+
const v = row[f.name];
|
|
308
|
+
const present = v !== undefined && v !== null && v !== '';
|
|
309
|
+
return (
|
|
310
|
+
<td key={f.name}
|
|
311
|
+
style={{ padding: '0.25rem 0.5rem',
|
|
312
|
+
color: present ? undefined : '#f59e0b' }}>
|
|
313
|
+
{present ? String(v) : '(not set)'}
|
|
314
|
+
</td>
|
|
315
|
+
);
|
|
316
|
+
})}
|
|
317
|
+
</tr>
|
|
318
|
+
);
|
|
319
|
+
})}
|
|
320
|
+
</tbody>
|
|
321
|
+
</table>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// -------------------------------------------------------------------------
|
|
328
|
+
// Per-axis calibration matrix. Renders when the schema declares
|
|
329
|
+
// per-axis calibration_fields AND a current calibration is on file.
|
|
330
|
+
// Reads from `currentCalibration.values.<key>.<field>`. Read-only.
|
|
331
|
+
// -------------------------------------------------------------------------
|
|
332
|
+
const SubLocationsCalibrationPanel: React.FC<{
|
|
333
|
+
schema: any;
|
|
334
|
+
currentCalibration: any | undefined;
|
|
335
|
+
}> = ({ schema, currentCalibration }) => {
|
|
336
|
+
if (!schema || !Array.isArray(schema.keys) || !Array.isArray(schema.calibration_fields)
|
|
337
|
+
|| schema.calibration_fields.length === 0) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
if (!currentCalibration) return null;
|
|
341
|
+
const values = (currentCalibration.values && typeof currentCalibration.values === 'object')
|
|
342
|
+
? currentCalibration.values as Record<string, any>
|
|
343
|
+
: {};
|
|
344
|
+
return (
|
|
345
|
+
<div>
|
|
346
|
+
<h4 style={{ margin: '0 0 0.5rem 0' }}>
|
|
347
|
+
Current Calibration — Per-axis Values
|
|
348
|
+
</h4>
|
|
349
|
+
<div style={{ overflowX: 'auto' }}>
|
|
350
|
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
|
351
|
+
<thead>
|
|
352
|
+
<tr>
|
|
353
|
+
<th style={{ textAlign: 'left', padding: '0.25rem 0.5rem',
|
|
354
|
+
borderBottom: '1px solid var(--surface-border)' }}>
|
|
355
|
+
{schema.key_label ?? 'Key'}
|
|
356
|
+
</th>
|
|
357
|
+
{schema.calibration_fields.map((f: any) => (
|
|
358
|
+
<th key={f.name}
|
|
359
|
+
style={{ textAlign: 'left', padding: '0.25rem 0.5rem',
|
|
360
|
+
borderBottom: '1px solid var(--surface-border)' }}>
|
|
361
|
+
{(f.label ?? f.name)}{f.units ? ` [${f.units}]` : ''}
|
|
362
|
+
</th>
|
|
363
|
+
))}
|
|
364
|
+
</tr>
|
|
365
|
+
</thead>
|
|
366
|
+
<tbody>
|
|
367
|
+
{schema.keys.map((key: string) => {
|
|
368
|
+
const row = (values[key] ?? {}) as Record<string, any>;
|
|
369
|
+
return (
|
|
370
|
+
<tr key={key}>
|
|
371
|
+
<td style={{ padding: '0.25rem 0.5rem', fontWeight: 600 }}>{key}</td>
|
|
372
|
+
{schema.calibration_fields.map((f: any) => {
|
|
373
|
+
const v = row[f.name];
|
|
374
|
+
const present = v !== undefined && v !== null && v !== '';
|
|
375
|
+
return (
|
|
376
|
+
<td key={f.name}
|
|
377
|
+
style={{ padding: '0.25rem 0.5rem',
|
|
378
|
+
color: present ? undefined : '#f59e0b' }}>
|
|
379
|
+
{present ? String(v) : '(not set)'}
|
|
380
|
+
</td>
|
|
381
|
+
);
|
|
382
|
+
})}
|
|
383
|
+
</tr>
|
|
384
|
+
);
|
|
385
|
+
})}
|
|
386
|
+
</tbody>
|
|
387
|
+
</table>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
};
|