@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.
- 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 +166 -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 +392 -0
- 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
|
+
};
|