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