@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
|
@@ -24,103 +24,12 @@ export interface ResultHistoryTableProps {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* Browser download shim: turn an inline CSV (already built by the server)
|
|
28
|
+
* into a transient blob URL, click it, and clean up. The ZIP path uses a
|
|
29
|
+
* direct `<a href=download_url>` instead — the server has already written
|
|
30
|
+
* the file to /downloads/ and we just point the browser at it.
|
|
31
31
|
*/
|
|
32
|
-
const
|
|
33
|
-
if (!blob || typeof blob !== 'object') return {};
|
|
34
|
-
if ('data' in blob && blob.data && typeof blob.data === 'object'
|
|
35
|
-
&& Object.values(blob.data).some(v => Array.isArray(v))) {
|
|
36
|
-
return blob.data as Record<string, any[]>;
|
|
37
|
-
}
|
|
38
|
-
return blob as Record<string, any[]>;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* RFC 4180 escape: quote only when the value contains comma, quote, or newline.
|
|
43
|
-
*/
|
|
44
|
-
const escapeCsv = (v: any): string => {
|
|
45
|
-
if (v === null || v === undefined) return '';
|
|
46
|
-
const s = String(v);
|
|
47
|
-
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Build the canonical column order for a set of cycle blobs: `t` first
|
|
52
|
-
* (canonical x-axis in every current schema), then the union of remaining
|
|
53
|
-
* array-valued keys in first-seen order across cycles.
|
|
54
|
-
*/
|
|
55
|
-
const unifyColumns = (blobs: Record<string, any[]>[]): string[] => {
|
|
56
|
-
const seen = new Set<string>();
|
|
57
|
-
const out: string[] = [];
|
|
58
|
-
for (const blob of blobs) {
|
|
59
|
-
for (const [k, v] of Object.entries(blob)) {
|
|
60
|
-
if (Array.isArray(v) && !seen.has(k)) {
|
|
61
|
-
seen.add(k);
|
|
62
|
-
out.push(k);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
out.sort((a, b) => (a === 't' ? -1 : b === 't' ? 1 : 0));
|
|
67
|
-
return out;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Concatenate one or more cycle envelopes into a single CSV. Adds a leading
|
|
72
|
-
* `cycle_index` column so downstream consumers can split groups apart again.
|
|
73
|
-
* Missing columns in a given cycle become empty cells.
|
|
74
|
-
*/
|
|
75
|
-
const cyclesToCsv = (cycles: Array<{ cycleIndex: number; blob: any }>): string => {
|
|
76
|
-
if (cycles.length === 0) return '';
|
|
77
|
-
const unwrapped = cycles.map(c => ({
|
|
78
|
-
cycleIndex: c.cycleIndex,
|
|
79
|
-
blob: unwrapEnvelope(c.blob),
|
|
80
|
-
}));
|
|
81
|
-
const columns = unifyColumns(unwrapped.map(u => u.blob));
|
|
82
|
-
if (columns.length === 0) return '';
|
|
83
|
-
|
|
84
|
-
const lines: string[] = [['cycle_index', ...columns].join(',')];
|
|
85
|
-
for (const { cycleIndex, blob } of unwrapped) {
|
|
86
|
-
const nRows = columns.reduce(
|
|
87
|
-
(min, c) => Array.isArray(blob[c]) ? Math.min(min, blob[c].length) : min,
|
|
88
|
-
Infinity,
|
|
89
|
-
);
|
|
90
|
-
const finite = Number.isFinite(nRows) ? (nRows as number) : 0;
|
|
91
|
-
for (let i = 0; i < finite; i++) {
|
|
92
|
-
const row = [escapeCsv(cycleIndex)];
|
|
93
|
-
for (const c of columns) {
|
|
94
|
-
const arr = blob[c];
|
|
95
|
-
row.push(escapeCsv(Array.isArray(arr) ? arr[i] : ''));
|
|
96
|
-
}
|
|
97
|
-
lines.push(row.join(','));
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return lines.join('\n');
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Single-blob CSV — kept for the filtered-data download path, which is
|
|
105
|
-
* not (yet) per-cycle on the server.
|
|
106
|
-
*/
|
|
107
|
-
const rawBlobToCsv = (blob: any): string => {
|
|
108
|
-
const unwrapped = unwrapEnvelope(blob);
|
|
109
|
-
return cyclesToCsv([{ cycleIndex: 0, blob: unwrapped }])
|
|
110
|
-
// Strip the synthetic cycle_index column for the legacy single-blob path.
|
|
111
|
-
.split('\n')
|
|
112
|
-
.map(line => {
|
|
113
|
-
const idx = line.indexOf(',');
|
|
114
|
-
return idx >= 0 ? line.slice(idx + 1) : '';
|
|
115
|
-
})
|
|
116
|
-
.join('\n');
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Browser download shim: turn a string into a transient blob URL, click it,
|
|
121
|
-
* and clean up. Works without any extra libraries.
|
|
122
|
-
*/
|
|
123
|
-
const downloadCsv = (filename: string, csv: string) => {
|
|
32
|
+
const downloadCsvBlob = (filename: string, csv: string) => {
|
|
124
33
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
125
34
|
const url = URL.createObjectURL(blob);
|
|
126
35
|
const a = document.createElement('a');
|
|
@@ -132,8 +41,9 @@ const downloadCsv = (filename: string, csv: string) => {
|
|
|
132
41
|
URL.revokeObjectURL(url);
|
|
133
42
|
};
|
|
134
43
|
|
|
135
|
-
type DownloadKind = '
|
|
44
|
+
type DownloadKind = 'data' | 'report';
|
|
136
45
|
type InFlight = { runId: string; kind: DownloadKind };
|
|
46
|
+
type ProjectDownloadKind = 'report' | 'archive';
|
|
137
47
|
|
|
138
48
|
export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) => {
|
|
139
49
|
const tis = useTis();
|
|
@@ -143,6 +53,7 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
143
53
|
const [tests, setTests] = useState<any[]>([]);
|
|
144
54
|
const [loading, setLoading] = useState(false);
|
|
145
55
|
const [downloading, setDownloading] = useState<InFlight | null>(null);
|
|
56
|
+
const [projectBusy, setProjectBusy] = useState<ProjectDownloadKind | null>(null);
|
|
146
57
|
const { invoke } = useContext(EventEmitterContext);
|
|
147
58
|
|
|
148
59
|
const loadTests = async () => {
|
|
@@ -182,93 +93,98 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
182
93
|
const handleDownload = async (rowData: any, kind: DownloadKind) => {
|
|
183
94
|
const runId = rowData?.run_id;
|
|
184
95
|
const rowMethodId = rowData?.method_id ?? methodId;
|
|
185
|
-
if (!runId || !rowMethodId) return;
|
|
96
|
+
if (!runId || !rowMethodId || !projectId) return;
|
|
186
97
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
const topic
|
|
192
|
-
|
|
193
|
-
const
|
|
98
|
+
// The server builds both CSVs end-to-end now. `Report` returns
|
|
99
|
+
// metadata + cycles + results (no traces). `Data` returns raw
|
|
100
|
+
// cycles concatenated with `filtered_`-prefixed columns paired
|
|
101
|
+
// against cycle 1.
|
|
102
|
+
const topic = kind === 'report' ? 'tis.export_test_csv'
|
|
103
|
+
: 'tis.export_test_data_csv';
|
|
104
|
+
const label = kind === 'report' ? 'test report' : 'test data';
|
|
194
105
|
|
|
195
106
|
setDownloading({ runId, kind });
|
|
196
107
|
try {
|
|
197
|
-
|
|
108
|
+
const resp: any = await invoke(topic as any, MessageType.Request, {
|
|
109
|
+
project_id: projectId, method_id: rowMethodId, run_id: runId,
|
|
110
|
+
} as any);
|
|
111
|
+
if (!resp?.success) {
|
|
112
|
+
console.warn(`${topic} failed`, runId, resp?.error_message);
|
|
113
|
+
alert(`Failed to build ${label} for ${runId}` +
|
|
114
|
+
(resp?.error_message ? `: ${resp.error_message}` : ''));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const csv = typeof resp.data?.csv === 'string' ? resp.data.csv : '';
|
|
118
|
+
if (!csv) {
|
|
119
|
+
alert(`${label} for ${runId} is empty.`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Trust the server-supplied filename — it already has the
|
|
123
|
+
// sanitized sample_id, project, method, and run components
|
|
124
|
+
// assembled with the same rules used for files on disk.
|
|
125
|
+
const filename = typeof resp.data?.filename === 'string' && resp.data.filename
|
|
126
|
+
? resp.data.filename
|
|
127
|
+
: `${projectId}_${rowMethodId}_${runId}_${kind}.csv`;
|
|
128
|
+
downloadCsvBlob(filename, csv);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`Failed to download ${label}`, err);
|
|
131
|
+
alert(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
132
|
+
} finally {
|
|
133
|
+
setDownloading(null);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
198
136
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
} as any,
|
|
205
|
-
);
|
|
206
|
-
const cycleEntries: any[] = listResp?.data?.cycles ?? [];
|
|
207
|
-
const cycleIdxs = cycleEntries
|
|
208
|
-
.filter(c => c?.name === 'trace' && typeof c?.cycle_index === 'number')
|
|
209
|
-
.map(c => c.cycle_index as number)
|
|
210
|
-
.sort((a, b) => a - b);
|
|
137
|
+
const handleProjectDownload = async (kind: ProjectDownloadKind) => {
|
|
138
|
+
if (!projectId) return;
|
|
139
|
+
const topic = kind === 'report' ? 'tis.export_project_csv'
|
|
140
|
+
: 'tis.export_project_zip';
|
|
141
|
+
const label = kind === 'report' ? 'project report' : 'project archive';
|
|
211
142
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const fetched = await Promise.all(cycleIdxs.map(async (ci) => {
|
|
230
|
-
const r: any = await invoke('tis.read_raw' as any, MessageType.Request, {
|
|
231
|
-
project_id: projectId, method_id: rowMethodId,
|
|
232
|
-
run_id: runId, name: 'trace', cycle_index: ci,
|
|
233
|
-
} as any);
|
|
234
|
-
return r?.success ? { cycleIndex: ci, blob: r.data } : null;
|
|
235
|
-
}));
|
|
236
|
-
const good = fetched.filter((x): x is { cycleIndex: number; blob: any } => x !== null);
|
|
237
|
-
if (good.length === 0) {
|
|
238
|
-
alert(`No ${label} cycles readable for ${runId}.`);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
csv = cyclesToCsv(good);
|
|
143
|
+
setProjectBusy(kind);
|
|
144
|
+
try {
|
|
145
|
+
const resp: any = await invoke(topic as any, MessageType.Request, {
|
|
146
|
+
project_id: projectId,
|
|
147
|
+
} as any);
|
|
148
|
+
if (!resp?.success) {
|
|
149
|
+
console.warn(`${topic} failed`, projectId, resp?.error_message);
|
|
150
|
+
alert(`Failed to build ${label}` +
|
|
151
|
+
(resp?.error_message ? `: ${resp.error_message}` : ''));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (kind === 'report') {
|
|
156
|
+
const csv = typeof resp.data?.csv === 'string' ? resp.data.csv : '';
|
|
157
|
+
if (!csv) {
|
|
158
|
+
alert(`Project ${projectId} has no tests to report.`);
|
|
159
|
+
return;
|
|
242
160
|
}
|
|
161
|
+
const filename = typeof resp.data?.filename === 'string' && resp.data.filename
|
|
162
|
+
? resp.data.filename
|
|
163
|
+
: `${projectId}_project_report.csv`;
|
|
164
|
+
downloadCsvBlob(filename, csv);
|
|
243
165
|
} else {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!
|
|
249
|
-
|
|
250
|
-
alert(`No ${label} available for ${runId}` +
|
|
251
|
-
(resp?.error_message ? `: ${resp.error_message}` : ''));
|
|
166
|
+
// Archive lives on the server's /downloads/ endpoint — point
|
|
167
|
+
// the browser at it directly. Anchor click triggers a GET
|
|
168
|
+
// with content-disposition: attachment.
|
|
169
|
+
const url = typeof resp.data?.download_url === 'string' ? resp.data.download_url : '';
|
|
170
|
+
if (!url) {
|
|
171
|
+
alert(`Server did not return a download URL for ${label}.`);
|
|
252
172
|
return;
|
|
253
173
|
}
|
|
254
|
-
|
|
174
|
+
const a = document.createElement('a');
|
|
175
|
+
a.href = url;
|
|
176
|
+
a.download = typeof resp.data?.filename === 'string'
|
|
177
|
+
? resp.data.filename
|
|
178
|
+
: `${projectId}_project_archive.zip`;
|
|
179
|
+
document.body.appendChild(a);
|
|
180
|
+
a.click();
|
|
181
|
+
a.remove();
|
|
255
182
|
}
|
|
256
|
-
|
|
257
|
-
if (!csv) {
|
|
258
|
-
alert(`${label} for ${runId} is empty or has no array columns.`);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
// sample_id is required for every run, so it always belongs in
|
|
262
|
-
// the filename. Sanitize to match the on-disk filename rules
|
|
263
|
-
// applied server-side in tis_servelet::sanitize_for_filename.
|
|
264
|
-
const sampleId = sanitizeForFilename(sampleIdOf(rowData));
|
|
265
|
-
const sampleSeg = sampleId ? `${sampleId}_` : '';
|
|
266
|
-
downloadCsv(`${projectId}_${rowMethodId}_${sampleSeg}${runId}_${suffix}.csv`, csv);
|
|
267
183
|
} catch (err) {
|
|
268
184
|
console.error(`Failed to download ${label}`, err);
|
|
269
185
|
alert(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
270
186
|
} finally {
|
|
271
|
-
|
|
187
|
+
setProjectBusy(null);
|
|
272
188
|
}
|
|
273
189
|
};
|
|
274
190
|
|
|
@@ -286,25 +202,47 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
286
202
|
return '';
|
|
287
203
|
};
|
|
288
204
|
|
|
289
|
-
// Mirrors tis_servelet::sanitize_for_filename on the server. Keeping
|
|
290
|
-
// the rules aligned means the SampleID segment in download names
|
|
291
|
-
// matches the on-disk raw_data/<sample_id>_<name>.json file.
|
|
292
|
-
const sanitizeForFilename = (s: string): string =>
|
|
293
|
-
s.replace(/[\/\\:*?"<>|\0\x00-\x1f]/g, '_');
|
|
294
|
-
|
|
295
205
|
return (
|
|
296
206
|
// Outer wrapper pins the whole component to its container's width.
|
|
297
207
|
// `overflow: hidden` keeps a wide row from pushing the table past the
|
|
298
208
|
// parent; DataTable's `scrollable` handles internal overflow via its
|
|
299
209
|
// own scrollbars instead of blowing out layout.
|
|
300
210
|
<div style={{ width: '100%', maxWidth: '100%', overflow: 'hidden', boxSizing: 'border-box' }}>
|
|
301
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
|
211
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
302
212
|
<h3 style={{ margin: 0 }}>
|
|
303
213
|
{projectId
|
|
304
214
|
? `Test History: ${projectId}${methodId ? ` / ${methodId}` : ''}`
|
|
305
215
|
: 'Test History (no project selected)'}
|
|
306
216
|
</h3>
|
|
307
|
-
<
|
|
217
|
+
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center' }}>
|
|
218
|
+
<Button
|
|
219
|
+
icon={projectBusy === 'report' ? 'pi pi-spin pi-spinner' : 'pi pi-file'}
|
|
220
|
+
label="Download Report"
|
|
221
|
+
size="small"
|
|
222
|
+
outlined
|
|
223
|
+
disabled={!projectId || projectBusy !== null}
|
|
224
|
+
onClick={() => handleProjectDownload('report')}
|
|
225
|
+
tooltip="Download a CSV report of every test in this project"
|
|
226
|
+
tooltipOptions={{ position: 'bottom' }}
|
|
227
|
+
/>
|
|
228
|
+
<Button
|
|
229
|
+
icon={projectBusy === 'archive' ? 'pi pi-spin pi-spinner' : 'pi pi-box'}
|
|
230
|
+
label="Download Archive"
|
|
231
|
+
size="small"
|
|
232
|
+
outlined
|
|
233
|
+
disabled={!projectId || projectBusy !== null}
|
|
234
|
+
onClick={() => handleProjectDownload('archive')}
|
|
235
|
+
tooltip="Download a ZIP of the entire project directory (all tests, raw data, configs)"
|
|
236
|
+
tooltipOptions={{ position: 'bottom' }}
|
|
237
|
+
/>
|
|
238
|
+
<Button
|
|
239
|
+
icon="pi pi-refresh"
|
|
240
|
+
label="Refresh"
|
|
241
|
+
size="small"
|
|
242
|
+
onClick={loadTests}
|
|
243
|
+
disabled={loading}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
308
246
|
</div>
|
|
309
247
|
|
|
310
248
|
<DataTable
|
|
@@ -355,29 +293,29 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
355
293
|
header="Download"
|
|
356
294
|
style={{ width: '14rem' }}
|
|
357
295
|
body={(rowData) => {
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const anyBusy
|
|
296
|
+
const isDataBusy = downloading?.runId === rowData.run_id && downloading?.kind === 'data';
|
|
297
|
+
const isReportBusy = downloading?.runId === rowData.run_id && downloading?.kind === 'report';
|
|
298
|
+
const anyBusy = downloading !== null;
|
|
361
299
|
return (
|
|
362
300
|
<div style={{ display: 'flex', gap: '0.4rem' }}>
|
|
363
301
|
<Button
|
|
364
|
-
icon={
|
|
365
|
-
label="
|
|
302
|
+
icon={isDataBusy ? 'pi pi-spin pi-spinner' : 'pi pi-download'}
|
|
303
|
+
label="Data"
|
|
366
304
|
size="small"
|
|
367
305
|
outlined
|
|
368
306
|
disabled={anyBusy}
|
|
369
|
-
onClick={() => handleDownload(rowData, '
|
|
370
|
-
tooltip="Download
|
|
307
|
+
onClick={() => handleDownload(rowData, 'data')}
|
|
308
|
+
tooltip="Download raw + filtered trace data as one CSV"
|
|
371
309
|
tooltipOptions={{ position: 'left' }}
|
|
372
310
|
/>
|
|
373
311
|
<Button
|
|
374
|
-
icon={
|
|
375
|
-
label="
|
|
312
|
+
icon={isReportBusy ? 'pi pi-spin pi-spinner' : 'pi pi-file'}
|
|
313
|
+
label="Report"
|
|
376
314
|
size="small"
|
|
377
315
|
outlined
|
|
378
316
|
disabled={anyBusy}
|
|
379
|
-
onClick={() => handleDownload(rowData, '
|
|
380
|
-
tooltip="Download
|
|
317
|
+
onClick={() => handleDownload(rowData, 'report')}
|
|
318
|
+
tooltip="Download a CSV report (metadata + cycles + results) for this test"
|
|
381
319
|
tooltipOptions={{ position: 'left' }}
|
|
382
320
|
/>
|
|
383
321
|
</div>
|