@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
@@ -24,103 +24,12 @@ export interface ResultHistoryTableProps {
24
24
  }
25
25
 
26
26
  /**
27
- * Peel a tis.read_raw response. The wire shape may be either a per-cycle
28
- * envelope `{ cycle_index, cycle_fields, context, data }` or the legacy
29
- * flat `{ col: number[] }` blob. CSV builders only want the columnar
30
- * `{ col: number[] }` part.
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 unwrapEnvelope = (blob: any): Record<string, any[]> => {
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 = 'raw' | 'filtered';
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
- // raw_data/ is per-cycle on disk; filtered_data/ is still a single
188
- // file per test. The raw path lists cycles via tis.list_raw and
189
- // concatenates them into one CSV with a leading cycle_index
190
- // column. Filtered keeps its one-shot tis.read_filtered.
191
- const topic = kind === 'raw' ? 'tis.read_raw' : 'tis.read_filtered';
192
- const label = kind === 'raw' ? 'raw trace' : 'filtered trace';
193
- const suffix = kind === 'raw' ? 'raw' : 'filtered';
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
- let csv = '';
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
- if (kind === 'raw') {
200
- // List every cycle on disk for this run/blob.
201
- const listResp: any = await invoke(
202
- 'tis.list_raw' as any, MessageType.Request, {
203
- project_id: projectId, method_id: rowMethodId, run_id: runId,
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
- if (cycleIdxs.length === 0) {
213
- // Legacy run with a single un-cycled file → fall back
214
- // to a one-shot read (server resolves the legacy path).
215
- const resp: any = await invoke(topic as any, MessageType.Request, {
216
- project_id: projectId, method_id: rowMethodId,
217
- run_id: runId, name: 'trace',
218
- } as any);
219
- if (!resp?.success || !resp.data) {
220
- console.warn(`${topic} returned no data for`, runId, resp?.error_message);
221
- alert(`No ${label} available for ${runId}` +
222
- (resp?.error_message ? `: ${resp.error_message}` : ''));
223
- return;
224
- }
225
- csv = rawBlobToCsv(resp.data);
226
- } else {
227
- // Fetch each cycle's envelope in parallel; concat into
228
- // one CSV with a leading cycle_index column.
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
- const resp: any = await invoke(topic as any, MessageType.Request, {
245
- project_id: projectId, method_id: rowMethodId,
246
- run_id: runId, name: 'trace',
247
- } as any);
248
- if (!resp?.success || !resp.data) {
249
- console.warn(`${topic} returned no data for`, runId, resp?.error_message);
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
- csv = rawBlobToCsv(resp.data);
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
- setDownloading(null);
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
- <Button icon="pi pi-refresh" label="Refresh" onClick={loadTests} disabled={loading} />
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 isRawBusy = downloading?.runId === rowData.run_id && downloading?.kind === 'raw';
359
- const isFilteredBusy = downloading?.runId === rowData.run_id && downloading?.kind === 'filtered';
360
- const anyBusy = downloading !== null;
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={isRawBusy ? 'pi pi-spin pi-spinner' : 'pi pi-download'}
365
- label="Raw"
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, 'raw')}
370
- tooltip="Download raw_data/trace.json as CSV"
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={isFilteredBusy ? 'pi pi-spin pi-spinner' : 'pi pi-download'}
375
- label="Filtered"
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, 'filtered')}
380
- tooltip="Download filtered_data/trace.json as CSV"
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>