@eclipse-lyra/extension-dataviewer 0.7.57 → 0.7.59
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/api.js +7 -7
- package/dist/api.js.map +1 -1
- package/dist/dataviewer-extension-nwaJ0wW-.js +710 -0
- package/dist/dataviewer-extension-nwaJ0wW-.js.map +1 -0
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/dataviewer-extension-xp4hbHtz.js +0 -746
- package/dist/dataviewer-extension-xp4hbHtz.js.map +0 -1
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { TOPIC_DATAVIEW_ADDED, TOPIC_DATAVIEW_PUBLISH } from "./api.js";
|
|
2
|
+
import { File, LyraPart, PANEL_BOTTOM, contributionRegistry, editorRegistry, filebrowserDialog, persistenceService, publish, rootContext, subscribe, toastError } from "@eclipse-lyra/core";
|
|
3
|
+
import { LitElement, css, html } from "lit";
|
|
4
|
+
import { v4 } from "@eclipse-lyra/core/externals/third-party";
|
|
5
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
6
|
+
import { when } from "@eclipse-lyra/core/externals/lit";
|
|
7
|
+
import _decorate from "@oxc-project/runtime/helpers/decorate";
|
|
8
|
+
import Papa from "papaparse";
|
|
9
|
+
//#region src/dataviewer-service.ts
|
|
10
|
+
var KEY_PREFIX = "dataview/";
|
|
11
|
+
var KEY_INDEX = KEY_PREFIX + "index";
|
|
12
|
+
var DataviewerService = class {
|
|
13
|
+
init() {
|
|
14
|
+
if (this.subscriptionToken !== void 0) return;
|
|
15
|
+
this.subscriptionToken = subscribe(TOPIC_DATAVIEW_PUBLISH, (payload) => {
|
|
16
|
+
this.handlePublish(payload);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async handlePublish(payload) {
|
|
20
|
+
const storageKey = v4();
|
|
21
|
+
const createdAt = Date.now();
|
|
22
|
+
const entry = {
|
|
23
|
+
id: payload.id ?? storageKey,
|
|
24
|
+
title: payload.title,
|
|
25
|
+
data: payload.data,
|
|
26
|
+
source: payload.source,
|
|
27
|
+
createdAt
|
|
28
|
+
};
|
|
29
|
+
await persistenceService.persistObject(KEY_PREFIX + storageKey, entry);
|
|
30
|
+
const index = await persistenceService.getObject(KEY_INDEX);
|
|
31
|
+
const list = Array.isArray(index) ? index : [];
|
|
32
|
+
list.push({
|
|
33
|
+
storageKey,
|
|
34
|
+
title: payload.title,
|
|
35
|
+
source: payload.source,
|
|
36
|
+
createdAt
|
|
37
|
+
});
|
|
38
|
+
await persistenceService.persistObject(KEY_INDEX, list);
|
|
39
|
+
publish(TOPIC_DATAVIEW_ADDED, {
|
|
40
|
+
storageKey,
|
|
41
|
+
title: payload.title,
|
|
42
|
+
createdAt
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async listViews() {
|
|
46
|
+
const raw = await persistenceService.getObject(KEY_INDEX);
|
|
47
|
+
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
48
|
+
if (typeof raw[0] === "string") return raw.map((storageKey) => ({
|
|
49
|
+
storageKey,
|
|
50
|
+
title: storageKey,
|
|
51
|
+
createdAt: 0
|
|
52
|
+
}));
|
|
53
|
+
return [...raw].sort((a, b) => a.createdAt - b.createdAt);
|
|
54
|
+
}
|
|
55
|
+
async getView(storageKey) {
|
|
56
|
+
return await persistenceService.getObject(KEY_PREFIX + storageKey) ?? null;
|
|
57
|
+
}
|
|
58
|
+
async deleteView(storageKey) {
|
|
59
|
+
const index = await persistenceService.getObject(KEY_INDEX);
|
|
60
|
+
const list = Array.isArray(index) ? index.filter((e) => e.storageKey !== storageKey) : [];
|
|
61
|
+
await persistenceService.persistObject(KEY_INDEX, list);
|
|
62
|
+
await persistenceService.persistObject(KEY_PREFIX + storageKey, null);
|
|
63
|
+
}
|
|
64
|
+
async clearAllViews() {
|
|
65
|
+
const index = await persistenceService.getObject(KEY_INDEX);
|
|
66
|
+
const list = Array.isArray(index) ? index : [];
|
|
67
|
+
await Promise.all(list.map((entry) => persistenceService.persistObject(KEY_PREFIX + entry.storageKey, null)));
|
|
68
|
+
await persistenceService.persistObject(KEY_INDEX, []);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var dataviewerService = new DataviewerService();
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/lyra-data-table.ts
|
|
74
|
+
var _LyraDataTable;
|
|
75
|
+
function cellString(value) {
|
|
76
|
+
if (value === null || value === void 0) return "";
|
|
77
|
+
return String(value);
|
|
78
|
+
}
|
|
79
|
+
function isNumericColumn(rows, colIndex) {
|
|
80
|
+
if (rows.length === 0) return false;
|
|
81
|
+
return rows.every((row) => {
|
|
82
|
+
const v = row[colIndex];
|
|
83
|
+
if (v === null || v === void 0) return true;
|
|
84
|
+
const n = Number(v);
|
|
85
|
+
return Number.isFinite(n);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function compareCells(a, b, numeric) {
|
|
89
|
+
if (numeric) {
|
|
90
|
+
const na = Number(a);
|
|
91
|
+
const nb = Number(b);
|
|
92
|
+
if (!Number.isFinite(na)) return Number.isFinite(nb) ? 1 : 0;
|
|
93
|
+
if (!Number.isFinite(nb)) return -1;
|
|
94
|
+
return na - nb;
|
|
95
|
+
}
|
|
96
|
+
return cellString(a).localeCompare(cellString(b), void 0, { numeric: true });
|
|
97
|
+
}
|
|
98
|
+
var LyraDataTable = class LyraDataTable extends LitElement {
|
|
99
|
+
static {
|
|
100
|
+
_LyraDataTable = this;
|
|
101
|
+
}
|
|
102
|
+
constructor(..._args) {
|
|
103
|
+
super(..._args);
|
|
104
|
+
this.data = {
|
|
105
|
+
columns: [],
|
|
106
|
+
rows: []
|
|
107
|
+
};
|
|
108
|
+
this.emptyMessage = "No data.";
|
|
109
|
+
this.sortColumnIndex = null;
|
|
110
|
+
this.sortDirection = "asc";
|
|
111
|
+
this.filterQuery = "";
|
|
112
|
+
this.pageSize = 25;
|
|
113
|
+
this.currentPage = 0;
|
|
114
|
+
}
|
|
115
|
+
static {
|
|
116
|
+
this.PAGE_SIZE_OPTIONS = [
|
|
117
|
+
10,
|
|
118
|
+
25,
|
|
119
|
+
50,
|
|
120
|
+
100
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
get columns() {
|
|
124
|
+
return Array.isArray(this.data?.columns) ? this.data.columns : [];
|
|
125
|
+
}
|
|
126
|
+
get rows() {
|
|
127
|
+
return Array.isArray(this.data?.rows) ? this.data.rows : [];
|
|
128
|
+
}
|
|
129
|
+
get filteredRows() {
|
|
130
|
+
const q = this.filterQuery.trim().toLowerCase();
|
|
131
|
+
if (!q) return this.rows;
|
|
132
|
+
return this.rows.filter((row) => row.some((cell) => cellString(cell).toLowerCase().includes(q)));
|
|
133
|
+
}
|
|
134
|
+
get sortedRows() {
|
|
135
|
+
const filtered = this.filteredRows;
|
|
136
|
+
if (this.sortColumnIndex == null || this.sortColumnIndex < 0) return filtered;
|
|
137
|
+
const col = this.sortColumnIndex;
|
|
138
|
+
const numeric = isNumericColumn(filtered, col);
|
|
139
|
+
const dir = this.sortDirection === "asc" ? 1 : -1;
|
|
140
|
+
return [...filtered].sort((rowA, rowB) => {
|
|
141
|
+
const a = rowA[col];
|
|
142
|
+
const b = rowB[col];
|
|
143
|
+
return dir * compareCells(a, b, numeric);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
get totalRows() {
|
|
147
|
+
return this.sortedRows.length;
|
|
148
|
+
}
|
|
149
|
+
get pageCount() {
|
|
150
|
+
const total = this.totalRows;
|
|
151
|
+
if (total === 0) return 1;
|
|
152
|
+
return Math.ceil(total / this.pageSize);
|
|
153
|
+
}
|
|
154
|
+
get pagedRows() {
|
|
155
|
+
const all = this.sortedRows;
|
|
156
|
+
const start = this.clampedPage * this.pageSize;
|
|
157
|
+
return all.slice(start, start + this.pageSize);
|
|
158
|
+
}
|
|
159
|
+
get clampedPage() {
|
|
160
|
+
const count = this.pageCount;
|
|
161
|
+
return count <= 0 ? 0 : Math.min(this.currentPage, count - 1);
|
|
162
|
+
}
|
|
163
|
+
goToPage(page) {
|
|
164
|
+
const last = Math.max(0, this.pageCount - 1);
|
|
165
|
+
this.currentPage = Math.max(0, Math.min(page, last));
|
|
166
|
+
this.requestUpdate();
|
|
167
|
+
}
|
|
168
|
+
onPageSizeChange(e) {
|
|
169
|
+
const val = e.target.value;
|
|
170
|
+
const n = parseInt(val, 10);
|
|
171
|
+
if (!Number.isFinite(n) || n < 1) return;
|
|
172
|
+
this.pageSize = n;
|
|
173
|
+
this.currentPage = 0;
|
|
174
|
+
this.requestUpdate();
|
|
175
|
+
}
|
|
176
|
+
onSort(colIndex) {
|
|
177
|
+
if (this.sortColumnIndex === colIndex) this.sortDirection = this.sortDirection === "asc" ? "desc" : "asc";
|
|
178
|
+
else {
|
|
179
|
+
this.sortColumnIndex = colIndex;
|
|
180
|
+
this.sortDirection = "asc";
|
|
181
|
+
}
|
|
182
|
+
this.requestUpdate();
|
|
183
|
+
}
|
|
184
|
+
onFilterInput(e) {
|
|
185
|
+
this.filterQuery = e.target.value;
|
|
186
|
+
this.requestUpdate();
|
|
187
|
+
}
|
|
188
|
+
clearFilter() {
|
|
189
|
+
this.filterQuery = "";
|
|
190
|
+
this.requestUpdate();
|
|
191
|
+
}
|
|
192
|
+
getSortAria(colIndex) {
|
|
193
|
+
if (this.sortColumnIndex !== colIndex) return "none";
|
|
194
|
+
return this.sortDirection === "asc" ? "ascending" : "descending";
|
|
195
|
+
}
|
|
196
|
+
render() {
|
|
197
|
+
const { columns } = this;
|
|
198
|
+
const total = this.totalRows;
|
|
199
|
+
const displayRows = this.pagedRows;
|
|
200
|
+
const page = this.clampedPage;
|
|
201
|
+
const count = this.pageCount;
|
|
202
|
+
const start = total === 0 ? 0 : page * this.pageSize + 1;
|
|
203
|
+
const end = Math.min((page + 1) * this.pageSize, total);
|
|
204
|
+
if (columns.length === 0 && total === 0 && this.rows.length === 0) return html`<div class="table-empty">${this.emptyMessage}</div>`;
|
|
205
|
+
return html`
|
|
206
|
+
<div class="table-toolbar">
|
|
207
|
+
<wa-input
|
|
208
|
+
class="filter-input"
|
|
209
|
+
placeholder="Filter…"
|
|
210
|
+
.value=${this.filterQuery}
|
|
211
|
+
@input=${this.onFilterInput}
|
|
212
|
+
@wa-clear=${this.clearFilter}
|
|
213
|
+
with-clear
|
|
214
|
+
size="small"
|
|
215
|
+
aria-label="Filter rows"
|
|
216
|
+
>
|
|
217
|
+
<wa-icon slot="start" name="magnifying-glass" label="Filter"></wa-icon>
|
|
218
|
+
</wa-input>
|
|
219
|
+
<div class="paging-controls">
|
|
220
|
+
<wa-select
|
|
221
|
+
class="page-size-select"
|
|
222
|
+
size="small"
|
|
223
|
+
.value=${String(this.pageSize)}
|
|
224
|
+
title="Rows per page"
|
|
225
|
+
@change=${this.onPageSizeChange}
|
|
226
|
+
>
|
|
227
|
+
${_LyraDataTable.PAGE_SIZE_OPTIONS.map((n) => html`<wa-option value=${String(n)}>${n}</wa-option>`)}
|
|
228
|
+
</wa-select>
|
|
229
|
+
<span class="paging-summary" aria-live="polite">
|
|
230
|
+
${total === 0 ? "0 rows" : `${start}–${end} of ${total}`}
|
|
231
|
+
</span>
|
|
232
|
+
<wa-button
|
|
233
|
+
size="small"
|
|
234
|
+
appearance="plain"
|
|
235
|
+
title="Previous page"
|
|
236
|
+
?disabled=${count <= 1 || page <= 0}
|
|
237
|
+
@click=${() => this.goToPage(page - 1)}
|
|
238
|
+
>
|
|
239
|
+
<wa-icon name="chevron-left" label="Previous"></wa-icon>
|
|
240
|
+
</wa-button>
|
|
241
|
+
<wa-button
|
|
242
|
+
size="small"
|
|
243
|
+
appearance="plain"
|
|
244
|
+
title="Next page"
|
|
245
|
+
?disabled=${count <= 1 || page >= count - 1}
|
|
246
|
+
@click=${() => this.goToPage(page + 1)}
|
|
247
|
+
>
|
|
248
|
+
<wa-icon name="chevron-right" label="Next"></wa-icon>
|
|
249
|
+
</wa-button>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="table-wrap">
|
|
253
|
+
<table class="result-table">
|
|
254
|
+
<thead>
|
|
255
|
+
<tr>
|
|
256
|
+
${columns.map((col, i) => html`
|
|
257
|
+
<th scope="col" role="columnheader" aria-sort=${this.getSortAria(i)}>
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
class="th-sort"
|
|
261
|
+
@click=${() => this.onSort(i)}
|
|
262
|
+
title="Sort by ${col}"
|
|
263
|
+
>
|
|
264
|
+
<span class="th-label">${col}</span>
|
|
265
|
+
${this.sortColumnIndex === i ? html`<wa-icon
|
|
266
|
+
name=${this.sortDirection === "asc" ? "arrow-up" : "arrow-down"}
|
|
267
|
+
label=${this.sortDirection}
|
|
268
|
+
></wa-icon>` : html`<wa-icon name="arrows-up-down" label="Sort"></wa-icon>`}
|
|
269
|
+
</button>
|
|
270
|
+
</th>
|
|
271
|
+
`)}
|
|
272
|
+
</tr>
|
|
273
|
+
</thead>
|
|
274
|
+
<tbody>
|
|
275
|
+
${displayRows.length === 0 ? html`<tr><td colspan=${columns.length} class="table-empty-cell">No matching rows.</td></tr>` : displayRows.map((row) => html`
|
|
276
|
+
<tr>
|
|
277
|
+
${row.map((cell) => html`<td>${cellString(cell)}</td>`)}
|
|
278
|
+
</tr>
|
|
279
|
+
`)}
|
|
280
|
+
</tbody>
|
|
281
|
+
</table>
|
|
282
|
+
</div>
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
static {
|
|
286
|
+
this.styles = css`
|
|
287
|
+
:host {
|
|
288
|
+
display: flex;
|
|
289
|
+
flex-direction: column;
|
|
290
|
+
height: 100%;
|
|
291
|
+
min-height: 0;
|
|
292
|
+
}
|
|
293
|
+
.table-empty {
|
|
294
|
+
flex: 1;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
justify-content: center;
|
|
298
|
+
padding: 1rem;
|
|
299
|
+
}
|
|
300
|
+
.table-toolbar {
|
|
301
|
+
flex: none;
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
gap: 0.75rem;
|
|
305
|
+
padding: 0.25rem 0;
|
|
306
|
+
flex-wrap: wrap;
|
|
307
|
+
}
|
|
308
|
+
.filter-input {
|
|
309
|
+
max-width: 280px;
|
|
310
|
+
}
|
|
311
|
+
.paging-controls {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
gap: 0.5rem;
|
|
315
|
+
margin-left: auto;
|
|
316
|
+
}
|
|
317
|
+
.page-size-select {
|
|
318
|
+
}
|
|
319
|
+
.paging-summary {
|
|
320
|
+
font-size: 0.8125rem;
|
|
321
|
+
color: var(--wa-color-text-quiet);
|
|
322
|
+
min-width: 5rem;
|
|
323
|
+
}
|
|
324
|
+
.table-wrap {
|
|
325
|
+
flex: 1;
|
|
326
|
+
min-height: 0;
|
|
327
|
+
overflow: auto;
|
|
328
|
+
border: 1px solid var(--wa-color-neutral-border-quiet);
|
|
329
|
+
border-radius: var(--wa-border-radius-medium, 0.25rem);
|
|
330
|
+
}
|
|
331
|
+
.result-table {
|
|
332
|
+
width: 100%;
|
|
333
|
+
border-collapse: collapse;
|
|
334
|
+
font-size: 0.875rem;
|
|
335
|
+
color: var(--wa-color-text-normal);
|
|
336
|
+
}
|
|
337
|
+
.result-table th,
|
|
338
|
+
.result-table td {
|
|
339
|
+
padding: 0.5rem 0.75rem;
|
|
340
|
+
text-align: left;
|
|
341
|
+
border-bottom: 1px solid var(--wa-color-neutral-border-quiet);
|
|
342
|
+
}
|
|
343
|
+
.result-table th {
|
|
344
|
+
position: sticky;
|
|
345
|
+
top: 0;
|
|
346
|
+
z-index: 1;
|
|
347
|
+
background: var(--wa-color-surface-lowered);
|
|
348
|
+
font-weight: 600;
|
|
349
|
+
white-space: nowrap;
|
|
350
|
+
color: var(--wa-color-text-normal);
|
|
351
|
+
box-shadow: 0 1px 0 0 var(--wa-color-neutral-border-quiet);
|
|
352
|
+
}
|
|
353
|
+
.result-table tbody tr:nth-child(even) td {
|
|
354
|
+
background: var(--wa-color-surface-default);
|
|
355
|
+
}
|
|
356
|
+
.result-table tbody tr:nth-child(odd) td {
|
|
357
|
+
background: var(--wa-color-surface-lowered);
|
|
358
|
+
}
|
|
359
|
+
.result-table tbody tr:hover td {
|
|
360
|
+
background: var(--wa-color-neutral-fill-normal);
|
|
361
|
+
}
|
|
362
|
+
.th-sort {
|
|
363
|
+
display: inline-flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
gap: 0.35rem;
|
|
366
|
+
width: 100%;
|
|
367
|
+
padding: 0;
|
|
368
|
+
border: none;
|
|
369
|
+
background: none;
|
|
370
|
+
font: inherit;
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
color: inherit;
|
|
373
|
+
text-align: left;
|
|
374
|
+
}
|
|
375
|
+
.th-sort:hover {
|
|
376
|
+
opacity: 0.85;
|
|
377
|
+
}
|
|
378
|
+
.th-label {
|
|
379
|
+
overflow: hidden;
|
|
380
|
+
text-overflow: ellipsis;
|
|
381
|
+
}
|
|
382
|
+
.th-sort wa-icon {
|
|
383
|
+
flex-shrink: 0;
|
|
384
|
+
opacity: 0.7;
|
|
385
|
+
font-size: 0.75em;
|
|
386
|
+
}
|
|
387
|
+
.table-empty-cell {
|
|
388
|
+
color: var(--wa-color-text-quiet);
|
|
389
|
+
font-style: italic;
|
|
390
|
+
text-align: center;
|
|
391
|
+
}
|
|
392
|
+
`;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
_decorate([property({ attribute: false })], LyraDataTable.prototype, "data", void 0);
|
|
396
|
+
_decorate([property({ type: String })], LyraDataTable.prototype, "emptyMessage", void 0);
|
|
397
|
+
_decorate([state()], LyraDataTable.prototype, "sortColumnIndex", void 0);
|
|
398
|
+
_decorate([state()], LyraDataTable.prototype, "sortDirection", void 0);
|
|
399
|
+
_decorate([state()], LyraDataTable.prototype, "filterQuery", void 0);
|
|
400
|
+
_decorate([state()], LyraDataTable.prototype, "pageSize", void 0);
|
|
401
|
+
_decorate([state()], LyraDataTable.prototype, "currentPage", void 0);
|
|
402
|
+
LyraDataTable = _LyraDataTable = _decorate([customElement("lyra-data-table")], LyraDataTable);
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/dataview-part.ts
|
|
405
|
+
var DataViewPart = class DataViewPart extends LyraPart {
|
|
406
|
+
constructor(..._args) {
|
|
407
|
+
super(..._args);
|
|
408
|
+
this.dataview = null;
|
|
409
|
+
this.persistedList = [];
|
|
410
|
+
this.selectedStorageKey = "";
|
|
411
|
+
this.selectedView = null;
|
|
412
|
+
this.loadingList = true;
|
|
413
|
+
this.autoActivateTab = true;
|
|
414
|
+
}
|
|
415
|
+
get displayed() {
|
|
416
|
+
return this.selectedView ?? this.dataview;
|
|
417
|
+
}
|
|
418
|
+
get hasData() {
|
|
419
|
+
const dv = this.displayed;
|
|
420
|
+
if (!dv) return false;
|
|
421
|
+
const { columns, rows } = dv.data;
|
|
422
|
+
return Array.isArray(columns) && Array.isArray(rows) && (columns.length > 0 || rows.length > 0);
|
|
423
|
+
}
|
|
424
|
+
toCsv(dv) {
|
|
425
|
+
const { columns, rows } = dv.data;
|
|
426
|
+
const escapeCell = (value) => {
|
|
427
|
+
if (value === null || value === void 0) return "";
|
|
428
|
+
const str = String(value);
|
|
429
|
+
if (!/[",\n]/.test(str)) return str;
|
|
430
|
+
return `"${str.replace(/"/g, "\"\"")}"`;
|
|
431
|
+
};
|
|
432
|
+
const header = columns.map(escapeCell).join(",");
|
|
433
|
+
const body = rows.map((row) => row.map(escapeCell).join(",")).join("\n");
|
|
434
|
+
return body ? `${header}\n${body}` : header;
|
|
435
|
+
}
|
|
436
|
+
async onExportCsv() {
|
|
437
|
+
const dv = this.displayed;
|
|
438
|
+
if (!dv || !this.hasData) return;
|
|
439
|
+
try {
|
|
440
|
+
const csv = this.toCsv(dv);
|
|
441
|
+
const safeTitle = dv.title?.trim() || "dataview";
|
|
442
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
443
|
+
const fileName = `${safeTitle.replace(/[^a-zA-Z0-9-_]+/g, "_")}-${timestamp}.csv`;
|
|
444
|
+
const targetDir = await filebrowserDialog("directory");
|
|
445
|
+
if (!targetDir) return;
|
|
446
|
+
this.executeCommand("touch", {
|
|
447
|
+
path: `${targetDir}/${fileName}`,
|
|
448
|
+
contents: csv
|
|
449
|
+
});
|
|
450
|
+
} catch (err) {
|
|
451
|
+
toastError(err instanceof Error ? err.message : String(err));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async doInitUI() {
|
|
455
|
+
const persisted = await this.getDialogSetting();
|
|
456
|
+
if (persisted && typeof persisted.autoActivateTab === "boolean") this.autoActivateTab = persisted.autoActivateTab;
|
|
457
|
+
this.subscribe(TOPIC_DATAVIEW_ADDED, async () => {
|
|
458
|
+
await this.refreshPersistedList(true);
|
|
459
|
+
if (this.autoActivateTab) this.activateContainingTab();
|
|
460
|
+
});
|
|
461
|
+
await this.refreshPersistedList(false);
|
|
462
|
+
}
|
|
463
|
+
async refreshPersistedList(selectLatest) {
|
|
464
|
+
this.loadingList = true;
|
|
465
|
+
this.requestUpdate();
|
|
466
|
+
try {
|
|
467
|
+
this.persistedList = await dataviewerService.listViews();
|
|
468
|
+
if (selectLatest && this.persistedList.length > 0) {
|
|
469
|
+
const latest = this.persistedList[this.persistedList.length - 1];
|
|
470
|
+
this.selectedStorageKey = latest.storageKey;
|
|
471
|
+
this.selectedView = await dataviewerService.getView(latest.storageKey);
|
|
472
|
+
} else if (this.selectedStorageKey) this.selectedView = await dataviewerService.getView(this.selectedStorageKey);
|
|
473
|
+
else this.selectedView = null;
|
|
474
|
+
} catch (e) {
|
|
475
|
+
toastError(e instanceof Error ? e.message : String(e));
|
|
476
|
+
this.persistedList = [];
|
|
477
|
+
this.selectedView = null;
|
|
478
|
+
} finally {
|
|
479
|
+
this.loadingList = false;
|
|
480
|
+
this.requestUpdate();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async selectStorageKey(key) {
|
|
484
|
+
this.selectedStorageKey = key;
|
|
485
|
+
if (!key) {
|
|
486
|
+
this.selectedView = null;
|
|
487
|
+
this.requestUpdate();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
this.selectedView = await dataviewerService.getView(key);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
toastError(err instanceof Error ? err.message : String(err));
|
|
494
|
+
this.selectedView = null;
|
|
495
|
+
}
|
|
496
|
+
this.requestUpdate();
|
|
497
|
+
}
|
|
498
|
+
async onAutoActivateChange(e) {
|
|
499
|
+
const checked = e.target.checked;
|
|
500
|
+
this.autoActivateTab = checked;
|
|
501
|
+
const current = await this.getDialogSetting() ?? {};
|
|
502
|
+
await this.setDialogSetting({
|
|
503
|
+
...current,
|
|
504
|
+
autoActivateTab: checked
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async onHistorySelect(e) {
|
|
508
|
+
const value = e.detail?.item?.value ?? "";
|
|
509
|
+
if (!value || value === "__stats__") return;
|
|
510
|
+
await this.selectStorageKey(value);
|
|
511
|
+
}
|
|
512
|
+
async onDeleteView(e, storageKey) {
|
|
513
|
+
e.stopPropagation();
|
|
514
|
+
e.preventDefault();
|
|
515
|
+
try {
|
|
516
|
+
await dataviewerService.deleteView(storageKey);
|
|
517
|
+
if (this.selectedStorageKey === storageKey) {
|
|
518
|
+
this.selectedStorageKey = "";
|
|
519
|
+
this.selectedView = null;
|
|
520
|
+
}
|
|
521
|
+
await this.refreshPersistedList(true);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
toastError(err instanceof Error ? err.message : String(err));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async onClearHistory() {
|
|
527
|
+
try {
|
|
528
|
+
await dataviewerService.clearAllViews();
|
|
529
|
+
this.selectedStorageKey = "";
|
|
530
|
+
this.selectedView = null;
|
|
531
|
+
await this.refreshPersistedList(false);
|
|
532
|
+
} catch (err) {
|
|
533
|
+
toastError(err instanceof Error ? err.message : String(err));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
renderToolbar() {
|
|
537
|
+
const current = this.selectedView ?? this.dataview;
|
|
538
|
+
const selectedMeta = this.persistedList.find((e) => e.storageKey === this.selectedStorageKey);
|
|
539
|
+
const baseTitle = selectedMeta?.title ?? current?.title ?? (this.persistedList.length > 0 ? "Latest data view" : "No data");
|
|
540
|
+
const formattedCreatedAt = selectedMeta?.createdAt ?? current?.createdAt ? new Date(selectedMeta?.createdAt ?? current?.createdAt).toLocaleString() : null;
|
|
541
|
+
const engineLabel = current?.source ?? null;
|
|
542
|
+
const titleWithEngine = engineLabel ? `${baseTitle} · ${engineLabel}` : baseTitle;
|
|
543
|
+
const currentLabel = formattedCreatedAt ? `${titleWithEngine} (${formattedCreatedAt})` : titleWithEngine;
|
|
544
|
+
return html`
|
|
545
|
+
<wa-dropdown
|
|
546
|
+
placement="bottom-start"
|
|
547
|
+
distance="4"
|
|
548
|
+
size="small"
|
|
549
|
+
hoist
|
|
550
|
+
@wa-select=${(e) => this.onHistorySelect(e)}
|
|
551
|
+
>
|
|
552
|
+
<wa-button
|
|
553
|
+
slot="trigger"
|
|
554
|
+
appearance="plain"
|
|
555
|
+
size="small"
|
|
556
|
+
with-caret
|
|
557
|
+
title="Data view history"
|
|
558
|
+
>
|
|
559
|
+
<wa-icon name="clock-rotate-left" label="History"></wa-icon>
|
|
560
|
+
</wa-button>
|
|
561
|
+
|
|
562
|
+
<wa-dropdown-item value="__stats__">
|
|
563
|
+
${this.persistedList.length} data view${this.persistedList.length === 1 ? "" : "s"}
|
|
564
|
+
${this.persistedList.length > 0 ? html`
|
|
565
|
+
<wa-button
|
|
566
|
+
slot="details"
|
|
567
|
+
appearance="plain"
|
|
568
|
+
size="small"
|
|
569
|
+
title="Clear history"
|
|
570
|
+
@click=${() => this.onClearHistory()}
|
|
571
|
+
>
|
|
572
|
+
<wa-icon name="trash" label="Clear history"></wa-icon>
|
|
573
|
+
</wa-button>
|
|
574
|
+
` : null}
|
|
575
|
+
</wa-dropdown-item>
|
|
576
|
+
|
|
577
|
+
${this.persistedList.map((entry) => html`
|
|
578
|
+
<wa-dropdown-item value=${entry.storageKey}>
|
|
579
|
+
${entry.source ? `${entry.title} · ${entry.source}` : entry.title}
|
|
580
|
+
${entry.createdAt ? html`<span style="opacity: 0.7; margin-left: 0.5rem; font-size: 0.75em;">
|
|
581
|
+
(${new Date(entry.createdAt).toLocaleString()})
|
|
582
|
+
</span>` : null}
|
|
583
|
+
<wa-button
|
|
584
|
+
slot="details"
|
|
585
|
+
appearance="plain"
|
|
586
|
+
size="small"
|
|
587
|
+
title="Delete data view"
|
|
588
|
+
@click=${(e) => this.onDeleteView(e, entry.storageKey)}
|
|
589
|
+
>
|
|
590
|
+
<wa-icon name="trash" label="Delete"></wa-icon>
|
|
591
|
+
</wa-button>
|
|
592
|
+
</wa-dropdown-item>
|
|
593
|
+
`)}
|
|
594
|
+
|
|
595
|
+
</wa-dropdown>
|
|
596
|
+
|
|
597
|
+
<wa-divider orientation="vertical"></wa-divider>
|
|
598
|
+
|
|
599
|
+
<wa-button
|
|
600
|
+
size="small"
|
|
601
|
+
appearance="plain"
|
|
602
|
+
title="Export current data view to CSV"
|
|
603
|
+
?disabled=${!this.hasData}
|
|
604
|
+
@click=${() => this.onExportCsv()}
|
|
605
|
+
>
|
|
606
|
+
<wa-icon name="file-csv" label="Export CSV"></wa-icon>
|
|
607
|
+
</wa-button>
|
|
608
|
+
|
|
609
|
+
<wa-switch
|
|
610
|
+
?checked=${this.autoActivateTab}
|
|
611
|
+
size="small"
|
|
612
|
+
title="Switch to this tab when new results arrive"
|
|
613
|
+
@change=${(e) => this.onAutoActivateChange(e)}
|
|
614
|
+
style="margin-top: 0.5rem;"
|
|
615
|
+
>
|
|
616
|
+
Auto-show
|
|
617
|
+
</wa-switch>
|
|
618
|
+
|
|
619
|
+
${when(current, () => html`<wa-divider orientation="vertical"></wa-divider><span>${currentLabel}</span>`)}
|
|
620
|
+
`;
|
|
621
|
+
}
|
|
622
|
+
renderTable(dv) {
|
|
623
|
+
if (!this.hasData) return html`<div class="result-empty">No data.</div>`;
|
|
624
|
+
return html`<lyra-data-table .data=${dv.data}></lyra-data-table>`;
|
|
625
|
+
}
|
|
626
|
+
renderContent() {
|
|
627
|
+
const dv = this.displayed;
|
|
628
|
+
if (dv != null) return this.renderTable(dv);
|
|
629
|
+
return html`<div class="result-empty">No data.</div>`;
|
|
630
|
+
}
|
|
631
|
+
static {
|
|
632
|
+
this.styles = css`
|
|
633
|
+
:host {
|
|
634
|
+
display: flex;
|
|
635
|
+
flex-direction: column;
|
|
636
|
+
height: 100%;
|
|
637
|
+
}
|
|
638
|
+
.result-empty {
|
|
639
|
+
flex: 1;
|
|
640
|
+
display: flex;
|
|
641
|
+
align-items: center;
|
|
642
|
+
justify-content: center;
|
|
643
|
+
padding: 1rem;
|
|
644
|
+
}
|
|
645
|
+
`;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
_decorate([property({ attribute: false })], DataViewPart.prototype, "dataview", void 0);
|
|
649
|
+
_decorate([state()], DataViewPart.prototype, "persistedList", void 0);
|
|
650
|
+
_decorate([state()], DataViewPart.prototype, "selectedStorageKey", void 0);
|
|
651
|
+
_decorate([state()], DataViewPart.prototype, "selectedView", void 0);
|
|
652
|
+
_decorate([state()], DataViewPart.prototype, "loadingList", void 0);
|
|
653
|
+
_decorate([state()], DataViewPart.prototype, "autoActivateTab", void 0);
|
|
654
|
+
DataViewPart = _decorate([customElement("lyra-dataview")], DataViewPart);
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/parse-csv.ts
|
|
657
|
+
/** Parses CSV-like (comma-, tab-, or other delimited) text; delimiter is auto-detected. */
|
|
658
|
+
function parseCsv(text) {
|
|
659
|
+
const result = Papa.parse(text, {
|
|
660
|
+
header: true,
|
|
661
|
+
skipEmptyLines: true
|
|
662
|
+
});
|
|
663
|
+
const columns = result.meta.fields ?? [];
|
|
664
|
+
return {
|
|
665
|
+
columns,
|
|
666
|
+
rows: result.data.map((row) => columns.map((col) => row[col]))
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/dataviewer-extension.ts
|
|
671
|
+
dataviewerService.init();
|
|
672
|
+
rootContext.put("dataviewerService", dataviewerService);
|
|
673
|
+
contributionRegistry.registerContribution(PANEL_BOTTOM, {
|
|
674
|
+
name: "view.dataviewer",
|
|
675
|
+
label: "Data Views",
|
|
676
|
+
icon: "table",
|
|
677
|
+
component: (id) => html`<lyra-dataview id="${id}"></lyra-dataview>`
|
|
678
|
+
});
|
|
679
|
+
editorRegistry.registerEditorInputHandler({
|
|
680
|
+
editorId: "system.dataviewer-table",
|
|
681
|
+
label: "Table",
|
|
682
|
+
icon: "table",
|
|
683
|
+
ranking: 800,
|
|
684
|
+
canHandle: (input) => {
|
|
685
|
+
if (!(input instanceof File)) return false;
|
|
686
|
+
const lower = input.getName().toLowerCase();
|
|
687
|
+
return lower.endsWith(".csv") || lower.endsWith(".tsv");
|
|
688
|
+
},
|
|
689
|
+
handle: async (input) => {
|
|
690
|
+
input.getName();
|
|
691
|
+
const { columns, rows } = parseCsv(await input.getContents() ?? "");
|
|
692
|
+
const data = {
|
|
693
|
+
columns,
|
|
694
|
+
rows
|
|
695
|
+
};
|
|
696
|
+
return {
|
|
697
|
+
title: input.getWorkspacePath(),
|
|
698
|
+
data,
|
|
699
|
+
key: input.getWorkspacePath(),
|
|
700
|
+
icon: "table",
|
|
701
|
+
state: {},
|
|
702
|
+
component: () => html`<lyra-data-table .data=${data}></lyra-data-table>`
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
function dataviewer_extension_default() {}
|
|
707
|
+
//#endregion
|
|
708
|
+
export { dataviewer_extension_default as default };
|
|
709
|
+
|
|
710
|
+
//# sourceMappingURL=dataviewer-extension-nwaJ0wW-.js.map
|