@adcops/autocore-react 3.3.75 → 3.3.77

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.
Files changed (56) hide show
  1. package/dist/components/Indicator.d.ts +29 -52
  2. package/dist/components/Indicator.d.ts.map +1 -1
  3. package/dist/components/Indicator.js +1 -1
  4. package/dist/components/ams/AmsProvider.d.ts +7 -0
  5. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  6. package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
  7. package/dist/components/ams/AssetDetailView.js +1 -1
  8. package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
  9. package/dist/components/ams/AssetRegistryTable.js +1 -1
  10. package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -1
  11. package/dist/components/ams/CalibrationEntryDialog.js +1 -1
  12. package/dist/components/ams/MissingAssetsBanner.d.ts +11 -0
  13. package/dist/components/ams/MissingAssetsBanner.d.ts.map +1 -0
  14. package/dist/components/ams/MissingAssetsBanner.js +1 -0
  15. package/dist/components/ams/PlaceholderHealthPanel.d.ts +3 -0
  16. package/dist/components/ams/PlaceholderHealthPanel.d.ts.map +1 -0
  17. package/dist/components/ams/PlaceholderHealthPanel.js +1 -0
  18. package/dist/components/ams/index.d.ts +2 -0
  19. package/dist/components/ams/index.d.ts.map +1 -1
  20. package/dist/components/ams/index.js +1 -1
  21. package/dist/components/index.d.ts +8 -0
  22. package/dist/components/index.d.ts.map +1 -1
  23. package/dist/components/index.js +1 -1
  24. package/dist/components/network/NetworkPanel.d.ts +8 -0
  25. package/dist/components/network/NetworkPanel.d.ts.map +1 -0
  26. package/dist/components/network/NetworkPanel.js +1 -0
  27. package/dist/components/network/NetworkProvider.d.ts +72 -0
  28. package/dist/components/network/NetworkProvider.d.ts.map +1 -0
  29. package/dist/components/network/NetworkProvider.js +1 -0
  30. package/dist/components/network/StagedChangeBanner.d.ts +8 -0
  31. package/dist/components/network/StagedChangeBanner.d.ts.map +1 -0
  32. package/dist/components/network/StagedChangeBanner.js +1 -0
  33. package/dist/components/network/index.d.ts +7 -0
  34. package/dist/components/network/index.d.ts.map +1 -0
  35. package/dist/components/network/index.js +1 -0
  36. package/dist/components/tis/ProjectManager.d.ts +7 -0
  37. package/dist/components/tis/ProjectManager.d.ts.map +1 -0
  38. package/dist/components/tis/ProjectManager.js +1 -0
  39. package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
  40. package/dist/components/tis/ResultHistoryTable.js +1 -1
  41. package/package.json +1 -1
  42. package/src/components/Indicator.tsx +166 -162
  43. package/src/components/ams/AmsProvider.tsx +7 -0
  44. package/src/components/ams/AssetDetailView.tsx +287 -4
  45. package/src/components/ams/AssetRegistryTable.tsx +325 -21
  46. package/src/components/ams/CalibrationEntryDialog.tsx +163 -30
  47. package/src/components/ams/MissingAssetsBanner.tsx +124 -0
  48. package/src/components/ams/PlaceholderHealthPanel.tsx +188 -0
  49. package/src/components/ams/index.ts +2 -0
  50. package/src/components/index.ts +26 -0
  51. package/src/components/network/NetworkPanel.tsx +363 -0
  52. package/src/components/network/NetworkProvider.tsx +349 -0
  53. package/src/components/network/StagedChangeBanner.tsx +101 -0
  54. package/src/components/network/index.ts +17 -0
  55. package/src/components/tis/ProjectManager.tsx +392 -0
  56. package/src/components/tis/ResultHistoryTable.tsx +126 -188
@@ -0,0 +1,392 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * <ProjectManager> — administrative panel for the active project.
5
+ *
6
+ * - Lists every test (run) in the project with a per-row Delete button.
7
+ * - One-shot "Delete Project" button that wipes the whole project tree
8
+ * (with a confirmation prompt — this is destructive and can't be undone).
9
+ * - "Download Archive" button that streams a ZIP of the project tree.
10
+ * - "Server Disk Space" panel showing free / total bytes for the
11
+ * filesystem hosting the TIS data directory.
12
+ *
13
+ * Mounts under the operator-facing Project tab, alongside <ProjectSelector>.
14
+ * Reads `selection.projectId` from <TisProvider> so it follows whatever
15
+ * project the operator has picked.
16
+ */
17
+
18
+ import React, { useCallback, useContext, useEffect, useState } from 'react';
19
+ import { Button } from 'primereact/button';
20
+ import { DataTable } from 'primereact/datatable';
21
+ import { Column } from 'primereact/column';
22
+ import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
23
+ import { ProgressBar } from 'primereact/progressbar';
24
+ import { EventEmitterContext } from '../../core/EventEmitterContext';
25
+ import { MessageType } from '../../hub/CommandMessage';
26
+ import { useTis } from './TisProvider';
27
+
28
+ export interface ProjectManagerProps {
29
+ /** Override the project scope. Defaults to `useTis().selection.projectId`. */
30
+ projectId?: string;
31
+ }
32
+
33
+ interface DiskUsage {
34
+ base_directory: string;
35
+ total_bytes: number;
36
+ free_bytes: number;
37
+ available_bytes: number;
38
+ used_bytes: number;
39
+ }
40
+
41
+ const formatBytes = (n: number): string => {
42
+ if (!Number.isFinite(n) || n <= 0) return '0 B';
43
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
44
+ let i = 0;
45
+ let v = n;
46
+ while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
47
+ return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${units[i]}`;
48
+ };
49
+
50
+ const formatDate = (s: string): string => {
51
+ if (!s) return '';
52
+ const d = new Date(s);
53
+ return Number.isNaN(d.getTime()) ? s : d.toLocaleString();
54
+ };
55
+
56
+ export const ProjectManager: React.FC<ProjectManagerProps> = (props) => {
57
+ const tis = useTis();
58
+ const projectId = props.projectId ?? tis.selection.projectId;
59
+ const { invoke } = useContext(EventEmitterContext);
60
+
61
+ const [tests, setTests] = useState<any[]>([]);
62
+ const [loading, setLoading] = useState(false);
63
+ const [deletingRunId, setDeletingRunId] = useState<string | null>(null);
64
+ const [deletingProject, setDeletingProject] = useState(false);
65
+ const [archiving, setArchiving] = useState(false);
66
+ const [disk, setDisk] = useState<DiskUsage | null>(null);
67
+ const [diskError, setDiskError] = useState<string>('');
68
+
69
+ const loadTests = useCallback(async () => {
70
+ if (!projectId) { setTests([]); return; }
71
+ setLoading(true);
72
+ try {
73
+ const resp: any = await invoke('tis.list_tests' as any, MessageType.Request, {
74
+ project_id: projectId,
75
+ } as any);
76
+ if (resp?.success && resp.data?.tests) {
77
+ setTests(resp.data.tests as any[]);
78
+ } else {
79
+ setTests([]);
80
+ }
81
+ } catch (e) {
82
+ console.error('[ProjectManager] tis.list_tests failed:', e);
83
+ setTests([]);
84
+ } finally {
85
+ setLoading(false);
86
+ }
87
+ }, [projectId, invoke]);
88
+
89
+ const loadDisk = useCallback(async () => {
90
+ try {
91
+ const resp: any = await invoke('tis.disk_usage' as any, MessageType.Request, {} as any);
92
+ if (resp?.success && resp.data) {
93
+ setDisk(resp.data as DiskUsage);
94
+ setDiskError('');
95
+ } else {
96
+ setDisk(null);
97
+ setDiskError(resp?.error_message ?? 'disk_usage failed');
98
+ }
99
+ } catch (e) {
100
+ setDisk(null);
101
+ setDiskError(e instanceof Error ? e.message : String(e));
102
+ }
103
+ }, [invoke]);
104
+
105
+ useEffect(() => { void loadTests(); }, [loadTests, tis.state.activeRunId]);
106
+ useEffect(() => { void loadDisk(); }, [loadDisk]);
107
+
108
+ const performDeleteTest = async (rowData: any) => {
109
+ const runId = rowData?.run_id;
110
+ const methodId = rowData?.method_id;
111
+ if (!projectId || !methodId || !runId) return;
112
+ setDeletingRunId(runId);
113
+ try {
114
+ const resp: any = await invoke('tis.delete_test' as any, MessageType.Request, {
115
+ project_id: projectId, method_id: methodId, run_id: runId,
116
+ } as any);
117
+ if (!resp?.success) {
118
+ alert(`Failed to delete test ${runId}` +
119
+ (resp?.error_message ? `: ${resp.error_message}` : ''));
120
+ return;
121
+ }
122
+ await loadTests();
123
+ // Pinned selection may now point at a vanished run; clear it so
124
+ // sibling views fall back to the active scalars instead of
125
+ // staying parked on a deleted record.
126
+ if (tis.selection.runId === runId) {
127
+ tis.setSelection({ runId: null });
128
+ }
129
+ } catch (e) {
130
+ alert(`Delete failed: ${e instanceof Error ? e.message : String(e)}`);
131
+ } finally {
132
+ setDeletingRunId(null);
133
+ }
134
+ };
135
+
136
+ const confirmDeleteTest = (rowData: any) => {
137
+ const runId = rowData?.run_id ?? '';
138
+ const sampleId = rowData?.sample_id ?? '';
139
+ confirmDialog({
140
+ header: 'Delete test',
141
+ icon: 'pi pi-exclamation-triangle',
142
+ acceptLabel: 'Delete',
143
+ rejectLabel: 'Cancel',
144
+ acceptClassName: 'p-button-danger',
145
+ message: (
146
+ <div>
147
+ <p style={{ marginTop: 0 }}>
148
+ Permanently delete run <code>{runId}</code>
149
+ {sampleId ? <> (sample <code>{sampleId}</code>)</> : null}?
150
+ </p>
151
+ <p style={{ marginBottom: 0, fontSize: '0.875rem', color: '#9ca3af' }}>
152
+ Removes <code>test.json</code>, cycles, raw and filtered
153
+ data for this run. This action cannot be undone.
154
+ </p>
155
+ </div>
156
+ ),
157
+ accept: () => { void performDeleteTest(rowData); },
158
+ });
159
+ };
160
+
161
+ const performDeleteProject = async () => {
162
+ if (!projectId) return;
163
+ setDeletingProject(true);
164
+ try {
165
+ const resp: any = await invoke('tis.delete_project' as any, MessageType.Request, {
166
+ project_id: projectId,
167
+ } as any);
168
+ if (!resp?.success) {
169
+ alert(`Failed to delete project ${projectId}` +
170
+ (resp?.error_message ? `: ${resp.error_message}` : ''));
171
+ return;
172
+ }
173
+ // Clear pins so nothing keeps trying to read this project's data.
174
+ tis.setSelection({ projectId: null, methodId: null, sampleId: null, runId: null });
175
+ await tis.refreshProjects();
176
+ await loadDisk();
177
+ setTests([]);
178
+ } catch (e) {
179
+ alert(`Delete failed: ${e instanceof Error ? e.message : String(e)}`);
180
+ } finally {
181
+ setDeletingProject(false);
182
+ }
183
+ };
184
+
185
+ const confirmDeleteProject = () => {
186
+ if (!projectId) return;
187
+ const n = tests.length;
188
+ confirmDialog({
189
+ header: 'Delete project',
190
+ icon: 'pi pi-exclamation-triangle',
191
+ acceptLabel: 'Delete Project',
192
+ rejectLabel: 'Cancel',
193
+ acceptClassName: 'p-button-danger',
194
+ message: (
195
+ <div>
196
+ <p style={{ marginTop: 0 }}>
197
+ Permanently delete project <code>{projectId}</code> and
198
+ all <strong>{n}</strong> test{n === 1 ? '' : 's'} inside it?
199
+ </p>
200
+ <p style={{ marginBottom: 0, fontSize: '0.875rem', color: '#9ca3af' }}>
201
+ This removes every method, run, cycle, and raw/filtered
202
+ blob under the project. Consider downloading the
203
+ archive first. This action cannot be undone.
204
+ </p>
205
+ </div>
206
+ ),
207
+ accept: () => { void performDeleteProject(); },
208
+ });
209
+ };
210
+
211
+ const downloadArchive = async () => {
212
+ if (!projectId) return;
213
+ setArchiving(true);
214
+ try {
215
+ const resp: any = await invoke('tis.export_project_zip' as any, MessageType.Request, {
216
+ project_id: projectId,
217
+ } as any);
218
+ if (!resp?.success) {
219
+ alert(`Failed to build project archive` +
220
+ (resp?.error_message ? `: ${resp.error_message}` : ''));
221
+ return;
222
+ }
223
+ const url = typeof resp.data?.download_url === 'string' ? resp.data.download_url : '';
224
+ if (!url) {
225
+ alert('Server did not return a download URL for the archive.');
226
+ return;
227
+ }
228
+ const a = document.createElement('a');
229
+ a.href = url;
230
+ a.download = typeof resp.data?.filename === 'string'
231
+ ? resp.data.filename
232
+ : `${projectId}_project_archive.zip`;
233
+ document.body.appendChild(a);
234
+ a.click();
235
+ a.remove();
236
+ } catch (e) {
237
+ alert(`Download failed: ${e instanceof Error ? e.message : String(e)}`);
238
+ } finally {
239
+ setArchiving(false);
240
+ }
241
+ };
242
+
243
+ const sampleIdOf = (row: any): string => {
244
+ const top = typeof row?.sample_id === 'string' ? row.sample_id : '';
245
+ if (top) return top;
246
+ const cfg = row?.config;
247
+ if (cfg && typeof cfg === 'object' && typeof cfg.sample_id === 'string') {
248
+ return cfg.sample_id;
249
+ }
250
+ return '';
251
+ };
252
+
253
+ const diskPercent = disk && disk.total_bytes > 0
254
+ ? Math.min(100, Math.round((disk.used_bytes / disk.total_bytes) * 100))
255
+ : 0;
256
+
257
+ return (
258
+ <div style={{ width: '100%', maxWidth: '100%', boxSizing: 'border-box' }}>
259
+ <ConfirmDialog />
260
+
261
+ <div style={{
262
+ display: 'flex',
263
+ justifyContent: 'space-between',
264
+ alignItems: 'center',
265
+ marginBottom: '1rem',
266
+ gap: '0.5rem',
267
+ flexWrap: 'wrap',
268
+ }}>
269
+ <h3 style={{ margin: 0 }}>
270
+ {projectId
271
+ ? `Project Manager: ${projectId}`
272
+ : 'Project Manager (no project selected)'}
273
+ </h3>
274
+ <div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center', flexWrap: 'wrap' }}>
275
+ <Button
276
+ icon={archiving ? 'pi pi-spin pi-spinner' : 'pi pi-box'}
277
+ label="Download Archive"
278
+ size="small"
279
+ outlined
280
+ disabled={!projectId || archiving || deletingProject}
281
+ onClick={downloadArchive}
282
+ tooltip="Download a ZIP of the entire project directory"
283
+ tooltipOptions={{ position: 'bottom' }}
284
+ />
285
+ <Button
286
+ icon={deletingProject ? 'pi pi-spin pi-spinner' : 'pi pi-trash'}
287
+ label="Delete Project"
288
+ size="small"
289
+ severity="danger"
290
+ disabled={!projectId || deletingProject || archiving}
291
+ onClick={confirmDeleteProject}
292
+ tooltip="Permanently delete the project and every test inside it"
293
+ tooltipOptions={{ position: 'bottom' }}
294
+ />
295
+ <Button
296
+ icon="pi pi-refresh"
297
+ label="Refresh"
298
+ size="small"
299
+ onClick={() => { void loadTests(); void loadDisk(); }}
300
+ disabled={loading}
301
+ />
302
+ </div>
303
+ </div>
304
+
305
+ {/* Server disk usage panel — visible regardless of project selection
306
+ so the operator can spot a near-full disk before staging a run. */}
307
+ <div style={{
308
+ marginBottom: '1rem',
309
+ padding: '0.75rem 1rem',
310
+ border: '1px solid #2a2a2a',
311
+ borderRadius: 4,
312
+ background: '#161616',
313
+ }}>
314
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: '0.5rem' }}>
315
+ <strong>Server Disk Space</strong>
316
+ {disk ? (
317
+ <span style={{ fontSize: '0.875rem', color: '#9ca3af' }}>
318
+ {formatBytes(disk.available_bytes)} free of {formatBytes(disk.total_bytes)}
319
+ {' '}({100 - diskPercent}% available)
320
+ </span>
321
+ ) : diskError ? (
322
+ <span style={{ fontSize: '0.875rem', color: '#f87171' }}>
323
+ {diskError}
324
+ </span>
325
+ ) : (
326
+ <span style={{ fontSize: '0.875rem', color: '#9ca3af' }}>Loading…</span>
327
+ )}
328
+ </div>
329
+ {disk && (
330
+ <>
331
+ <ProgressBar
332
+ value={diskPercent}
333
+ showValue={false}
334
+ style={{ height: '0.5rem', marginTop: '0.5rem' }}
335
+ color={diskPercent >= 90 ? '#dc2626' : diskPercent >= 75 ? '#f59e0b' : undefined}
336
+ />
337
+ <div style={{ fontSize: '0.75rem', color: '#6b7280', marginTop: '0.4rem' }}>
338
+ <code>{disk.base_directory}</code>
339
+ </div>
340
+ </>
341
+ )}
342
+ </div>
343
+
344
+ <DataTable
345
+ value={tests}
346
+ loading={loading}
347
+ paginator
348
+ rows={10}
349
+ emptyMessage={projectId ? 'No tests in this project.' : 'Select a project to manage.'}
350
+ scrollable
351
+ scrollHeight="flex"
352
+ tableStyle={{ minWidth: 0 }}
353
+ style={{ width: '100%' }}
354
+ >
355
+ <Column header="Sample ID" body={sampleIdOf}
356
+ sortable
357
+ sortFunction={(e) => {
358
+ const copy = [...e.data];
359
+ copy.sort((a, b) => sampleIdOf(a).localeCompare(sampleIdOf(b)) * (e.order ?? 1));
360
+ return copy;
361
+ }}
362
+ style={{ minWidth: '8rem' }} />
363
+ <Column field="start_time" header="Date/Time" sortable
364
+ body={(r) => formatDate(r.start_time)}
365
+ style={{ minWidth: '12rem' }} />
366
+ <Column field="method_id" header="Test Method" sortable style={{ minWidth: '10rem' }} />
367
+ <Column field="run_id" header="Run ID" sortable style={{ minWidth: '12rem' }} />
368
+ <Column
369
+ header="Action"
370
+ style={{ width: '8rem' }}
371
+ body={(rowData) => {
372
+ const isBusy = deletingRunId === rowData.run_id;
373
+ const anyBusy = deletingRunId !== null;
374
+ return (
375
+ <Button
376
+ icon={isBusy ? 'pi pi-spin pi-spinner' : 'pi pi-trash'}
377
+ label="Delete"
378
+ size="small"
379
+ severity="danger"
380
+ outlined
381
+ disabled={anyBusy || deletingProject}
382
+ onClick={() => confirmDeleteTest(rowData)}
383
+ tooltip="Permanently delete this test"
384
+ tooltipOptions={{ position: 'left' }}
385
+ />
386
+ );
387
+ }}
388
+ />
389
+ </DataTable>
390
+ </div>
391
+ );
392
+ };