@raclettejs/workbench 0.1.35 → 0.1.36-canary.0

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 (38) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/config/compositions.js +18 -0
  3. package/config/interactionLinks.js +19 -0
  4. package/i18n/de-DE.json +15 -1
  5. package/i18n/en-EU.json +52 -1
  6. package/i18n/sk-SK.json +15 -1
  7. package/package.json +3 -4
  8. package/packages.json +3 -1
  9. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionConfiguration.vue +96 -14
  10. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionIntegration.vue +29 -1
  11. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetRenderer.vue +1 -6
  12. package/plugins/pacifico__compositions/frontend/composables/prefillInteractionLinkFromComposition.ts +54 -0
  13. package/plugins/pacifico__compositions/frontend/widgets/CompositionCreateWidget.vue +145 -9
  14. package/plugins/pacifico__compositions/frontend/widgets/CompositionEditWidget.vue +11 -2
  15. package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +25 -5
  16. package/plugins/pacifico__core/frontend/generated-config.ts +22 -0
  17. package/plugins/pacifico__interactionLinks/frontend/components/InteractionLinkConfiguration.vue +3 -65
  18. package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +20 -3
  19. package/plugins/raclette__cache/backend/helpers/cacheEntryBuilder.ts +94 -0
  20. package/plugins/raclette__cache/backend/helpers/cacheEntrySanitizer.ts +86 -0
  21. package/plugins/raclette__cache/backend/helpers/valkeyExplorer.ts +89 -0
  22. package/plugins/raclette__cache/backend/index.ts +21 -0
  23. package/plugins/raclette__cache/backend/routes/route.cache.cacheEntries.get.ts +118 -0
  24. package/plugins/raclette__cache/backend/routes/route.cache.cacheEntry.get.ts +91 -0
  25. package/plugins/raclette__cache/backend/routes.ts +8 -0
  26. package/plugins/raclette__cache/frontend/components/TableColumnConfigDialog.vue +95 -0
  27. package/plugins/raclette__cache/frontend/composables/useTableColumnConfig.ts +66 -0
  28. package/plugins/raclette__cache/frontend/generated-config.ts +33 -0
  29. package/plugins/raclette__cache/frontend/utils/cacheValueTableRows.ts +340 -0
  30. package/plugins/raclette__cache/frontend/widgets/CacheListWidget.vue +742 -0
  31. package/plugins/raclette__cache/raclette.plugin.ts +8 -0
  32. package/services/frontend/src/app/components/dynamicForm/DynamicForm.vue +3 -1
  33. package/services/frontend/src/app/components/interactionLink/InteractionLinkBehaviorForm.vue +99 -0
  34. package/services/frontend/src/app/components/stepNavigator/StepNavigator.vue +175 -24
  35. package/services/frontend/src/app/components/stepNavigator/StepNavigatorTypes.ts +18 -0
  36. package/services/frontend/src/app/composables/useInteractionLinkBehaviorFields.ts +83 -0
  37. package/services/frontend/src/app/composables/useRacletteUserIsAdmin.ts +25 -0
  38. package/services/frontend/src/orchestrator/assets/styles/themes/light.ts +1 -1
@@ -0,0 +1,742 @@
1
+ <template>
2
+ <div class="tw:p-4 tw:pt-2">
3
+ <v-alert
4
+ v-if="showValueTruncatedWarning"
5
+ type="warning"
6
+ density="compact"
7
+ class="tw:mb-3"
8
+ :text="$t('workbench.cacheList.truncatedDetailHint')"
9
+ />
10
+
11
+ <template v-if="!isValueView">
12
+ <BaseDataTable
13
+ :items="tableItems"
14
+ :exportable-items="exportableItems"
15
+ :loading="isLoading"
16
+ :headers="listHeaders"
17
+ :data-name="$t('workbench.cacheList.dataName')"
18
+ :show-filters="true"
19
+ :show-actions-column="true"
20
+ :actions-header-title="$t('workbench.cacheList.tableActions')"
21
+ :group-by="listAllKeys ? 'keyGroup' : undefined"
22
+ item-value="_id"
23
+ :row-click-handler="openDetail"
24
+ >
25
+ <template #toolbar-leading>
26
+ <v-select
27
+ v-model="db"
28
+ density="compact"
29
+ hide-details
30
+ variant="outlined"
31
+ class="tw:min-w-[5rem]"
32
+ :items="dbOptions"
33
+ :label="$t('workbench.cacheList.dbLabel')"
34
+ @update:model-value="reloadFromStart"
35
+ />
36
+ <v-text-field
37
+ v-model="pattern"
38
+ density="compact"
39
+ hide-details
40
+ variant="outlined"
41
+ class="tw:min-w-[14rem]"
42
+ :label="$t('workbench.cacheList.patternLabel')"
43
+ :placeholder="$t('workbench.cacheList.patternPlaceholder')"
44
+ prepend-inner-icon="mdi-magnify"
45
+ clearable
46
+ @keyup.enter="reloadFromStart"
47
+ @click:clear="reloadFromStart"
48
+ />
49
+ <v-select
50
+ v-model="limit"
51
+ density="compact"
52
+ hide-details
53
+ variant="outlined"
54
+ class="tw:min-w-[8rem]"
55
+ :items="batchSizeOptions"
56
+ :label="$t('workbench.cacheList.batchSizeLabel')"
57
+ @update:model-value="reloadFromStart"
58
+ />
59
+ <v-autocomplete
60
+ v-model="pickerKey"
61
+ density="compact"
62
+ hide-details
63
+ variant="outlined"
64
+ class="tw:min-w-[16rem]"
65
+ :items="keyPickerItems"
66
+ :label="$t('workbench.cacheList.keyPickerLabel')"
67
+ @update:model-value="onKeyPickerSelect"
68
+ />
69
+ <v-btn
70
+ variant="tonal"
71
+ prepend-icon="mdi-refresh"
72
+ :text="$t('workbench.cacheList.refresh')"
73
+ :loading="isLoading"
74
+ @click="reloadFromStart"
75
+ />
76
+ </template>
77
+ <template #toolbar-end>
78
+ <v-btn
79
+ v-if="hasMore"
80
+ variant="outlined"
81
+ prepend-icon="mdi-chevron-down"
82
+ :text="$t('workbench.cacheList.loadMore')"
83
+ :loading="isLoadingMore"
84
+ @click="loadMore"
85
+ />
86
+ <span
87
+ v-if="total !== null"
88
+ class="tw:text-sm tw:text-medium-emphasis tw:whitespace-nowrap"
89
+ >
90
+ {{
91
+ $t("workbench.cacheList.showingCount", {
92
+ shown: tableItems.length,
93
+ total,
94
+ })
95
+ }}
96
+ ·
97
+ {{ $t("workbench.cacheList.dbActive", { db: activeDb }) }}
98
+ </span>
99
+ </template>
100
+ <template #item.value="{ item }">
101
+ <code class="tw:text-xs tw:break-all tw:line-clamp-2">
102
+ {{ displayValue(item) }}
103
+ </code>
104
+ <v-chip
105
+ v-if="item.truncated"
106
+ size="x-small"
107
+ color="warning"
108
+ class="tw:ml-1"
109
+ :text="$t('workbench.cacheList.truncated')"
110
+ />
111
+ </template>
112
+ <template #row-actions="{ item }">
113
+ <v-tooltip
114
+ :text="$t('workbench.cacheList.openValueTable')"
115
+ location="top"
116
+ >
117
+ <template #activator="{ props: tooltipProps }">
118
+ <v-btn
119
+ v-bind="tooltipProps"
120
+ icon="mdi-table"
121
+ size="small"
122
+ variant="text"
123
+ color="primary"
124
+ :aria-label="openValueTableAriaLabel(item)"
125
+ @click.stop.prevent="openValueTable(item)"
126
+ />
127
+ </template>
128
+ </v-tooltip>
129
+ </template>
130
+ </BaseDataTable>
131
+
132
+ <div class="tw:flex tw:justify-center tw:mt-2">
133
+ <v-btn
134
+ prepend-icon="mdi-table-cog"
135
+ rounded
136
+ variant="tonal"
137
+ color="primary"
138
+ @click="listConfigureDialogOpen = true"
139
+ >
140
+ {{ $t("workbench.cacheList.customizeTable") }}
141
+ <template v-if="listDisabledHeaders.length" #append>
142
+ ({{ listDisabledHeaders.length }})
143
+ </template>
144
+ </v-btn>
145
+ </div>
146
+
147
+ <TableColumnConfigDialog
148
+ v-model="listConfigureDialogOpen"
149
+ :headers="listAllHeaders"
150
+ :disabled-headers="listDisabledHeaders"
151
+ @toggle-header="handleToggleListHeader"
152
+ @select-all="handleSelectAllListHeaders"
153
+ @deselect-all="handleDeselectAllListHeaders"
154
+ />
155
+ </template>
156
+
157
+ <template v-else>
158
+ <div class="tw:flex tw:flex-wrap tw:gap-2 tw:items-center tw:justify-between tw:mb-3">
159
+ <div class="tw:flex tw:flex-wrap tw:gap-2 tw:items-center">
160
+ <v-btn
161
+ variant="tonal"
162
+ prepend-icon="mdi-arrow-left"
163
+ :text="$t('workbench.cacheList.backToList')"
164
+ @click="exitValueView"
165
+ />
166
+ <v-autocomplete
167
+ v-model="pickerKey"
168
+ density="compact"
169
+ hide-details
170
+ variant="outlined"
171
+ class="tw:min-w-[16rem]"
172
+ :items="keyPickerItems"
173
+ :label="$t('workbench.cacheList.keyPickerLabel')"
174
+ clearable
175
+ @update:model-value="onKeyPickerSelect"
176
+ @click:clear="exitValueView"
177
+ />
178
+ </div>
179
+ <span
180
+ v-if="valueTableEntry"
181
+ class="tw:text-sm tw:text-medium-emphasis tw:whitespace-nowrap"
182
+ >
183
+ {{ $t("workbench.cacheList.detailTtl") }}:
184
+ {{ valueTableEntry.ttlLabel }}
185
+ ·
186
+ {{ $t("workbench.cacheList.dbActive", { db: activeDb }) }}
187
+ </span>
188
+ </div>
189
+
190
+ <v-progress-linear
191
+ v-if="isLoadingCacheEntry"
192
+ indeterminate
193
+ class="tw:mb-3"
194
+ />
195
+
196
+ <BaseDataTable
197
+ v-if="!valueShowPrettyPrint"
198
+ :items="valueTableItems"
199
+ :headers="visibleValueTableHeaders"
200
+ :loading="isLoadingCacheEntry"
201
+ :data-name="valueTableDataName"
202
+ :show-filters="valueTableItems.length > 1"
203
+ :show-export="false"
204
+ :show-actions-column="false"
205
+ item-value="_id"
206
+ >
207
+ <template
208
+ v-for="header in visibleValueTableHeaders"
209
+ :key="header.key"
210
+ #[`item.${header.key}`]="{ item }"
211
+ >
212
+ <code class="tw:text-xs tw:break-all tw:whitespace-pre-wrap">{{
213
+ formatValueTableCell(item[header.key])
214
+ }}</code>
215
+ </template>
216
+ </BaseDataTable>
217
+
218
+ <pre
219
+ v-else-if="valueTableEntry && !isLoadingCacheEntry"
220
+ class="tw:text-xs tw:overflow-auto tw:max-h-[calc(100vh-16rem)] tw:p-3 tw:rounded tw:bg-surface-variant"
221
+ >{{ formatDetailValue(valueTableEntry) }}</pre>
222
+
223
+ <div v-if="!valueShowPrettyPrint" class="tw:flex tw:justify-center tw:mt-2">
224
+ <v-btn
225
+ prepend-icon="mdi-table-cog"
226
+ rounded
227
+ variant="tonal"
228
+ color="primary"
229
+ @click="valueConfigureDialogOpen = true"
230
+ >
231
+ {{ $t("workbench.cacheList.customizeTable") }}
232
+ <template v-if="valueDisabledHeaders.length" #append>
233
+ ({{ valueDisabledHeaders.length }})
234
+ </template>
235
+ </v-btn>
236
+ </div>
237
+
238
+ <TableColumnConfigDialog
239
+ v-if="!valueShowPrettyPrint"
240
+ v-model="valueConfigureDialogOpen"
241
+ :headers="valueTableAllHeaders"
242
+ :disabled-headers="valueDisabledHeaders"
243
+ @toggle-header="handleToggleValueHeader"
244
+ @select-all="handleSelectAllValueHeaders"
245
+ @deselect-all="handleDeselectAllValueHeaders"
246
+ />
247
+ </template>
248
+
249
+ <v-dialog v-model="detailOpen" max-width="720">
250
+ <v-card v-if="selectedEntry">
251
+ <v-card-title class="tw:break-all">
252
+ {{ selectedEntry.key }}
253
+ </v-card-title>
254
+ <v-card-subtitle>
255
+ {{ $t("workbench.cacheList.detailTtl") }}:
256
+ {{ selectedEntry.ttlLabel }}
257
+ </v-card-subtitle>
258
+ <v-card-text>
259
+ <v-alert
260
+ v-if="selectedEntry.truncated"
261
+ type="warning"
262
+ density="compact"
263
+ class="tw:mb-3"
264
+ :text="$t('workbench.cacheList.truncatedDetailHint')"
265
+ />
266
+ <pre class="tw:text-xs tw:overflow-auto tw:max-h-[24rem]">{{
267
+ formatDetailValue(selectedEntry)
268
+ }}</pre>
269
+ </v-card-text>
270
+ <v-card-actions>
271
+ <v-btn
272
+ variant="tonal"
273
+ color="primary"
274
+ prepend-icon="mdi-table"
275
+ :text="$t('workbench.cacheList.openValueTable')"
276
+ @click="openValueTableFromDetail"
277
+ />
278
+ <v-spacer />
279
+ <v-btn :text="$t('workbench.global.back')" @click="detailOpen = false" />
280
+ </v-card-actions>
281
+ </v-card>
282
+ </v-dialog>
283
+ </div>
284
+ </template>
285
+
286
+ <script setup lang="ts">
287
+ import { usePluginApi } from "@raclettejs/core/orchestrator/composables"
288
+ import BaseDataTable from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTable.vue"
289
+ import TableColumnConfigDialog from "../components/TableColumnConfigDialog.vue"
290
+ import { useTableColumnConfig } from "../composables/useTableColumnConfig"
291
+ import {
292
+ CACHE_KEY_COLUMN,
293
+ cacheEntriesValueTableRows,
294
+ isScalarFallbackResult,
295
+ type ValueTableHeader,
296
+ } from "../utils/cacheValueTableRows"
297
+ import { computed, onMounted, ref } from "vue"
298
+ import { useI18n } from "vue-i18n"
299
+
300
+ type CacheEntry = {
301
+ _id: string
302
+ key: string
303
+ keyGroup: string
304
+ value: string
305
+ valuePreview?: string
306
+ truncated: boolean
307
+ valueParsed?: unknown
308
+ ttl: number
309
+ ttlLabel: string
310
+ }
311
+
312
+ type CacheEntryPayloadItem = {
313
+ _id?: string
314
+ _source?: CacheEntry
315
+ }
316
+
317
+ type CacheEntriesBody = {
318
+ items?: CacheEntryPayloadItem[]
319
+ hits?: {
320
+ total: number
321
+ offset: number
322
+ limit: number
323
+ hasMore: boolean
324
+ instanceId: string
325
+ pattern: string
326
+ listAll: boolean
327
+ db: number
328
+ }
329
+ }
330
+
331
+ type CacheEntryBody = {
332
+ items?: CacheEntryPayloadItem[]
333
+ hits?: {
334
+ total: number
335
+ key: string
336
+ db: number
337
+ found: boolean
338
+ }
339
+ }
340
+
341
+ type CacheEntriesResponse = {
342
+ body?: CacheEntriesBody
343
+ }
344
+
345
+ type CacheEntryResponse = {
346
+ body?: CacheEntryBody
347
+ }
348
+
349
+ type ExecuteResult = {
350
+ result?: CacheEntry[] | { _source?: CacheEntry }[]
351
+ /** Axios response (useStore) or payload (useStore: false). */
352
+ response?: CacheEntriesResponse | CacheEntryResponse | { data?: CacheEntriesResponse | CacheEntryResponse }
353
+ }
354
+
355
+ const responsePayload = (
356
+ result: ExecuteResult | undefined,
357
+ ): CacheEntriesResponse | CacheEntryResponse | undefined => {
358
+ const raw = result?.response
359
+ if (!raw) return undefined
360
+ if ("body" in raw) return raw as CacheEntriesResponse | CacheEntryResponse
361
+ return (raw as { data?: CacheEntriesResponse | CacheEntryResponse }).data
362
+ }
363
+
364
+ defineProps<{
365
+ uuid: string
366
+ }>()
367
+
368
+ const { t } = useI18n()
369
+ const { $data } = usePluginApi()
370
+
371
+ const { execute, isLoading, clear } = $data.cacheEntries.get({
372
+ options: { useStore: true },
373
+ })
374
+
375
+ const { execute: fetchCacheEntry, isLoading: isLoadingCacheEntry } =
376
+ $data.cacheEntry.get({
377
+ options: { useStore: false },
378
+ })
379
+
380
+ const pattern = ref("")
381
+ const db = ref(0)
382
+ const dbOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
383
+ const activeDb = ref(0)
384
+ const listAllKeys = ref(true)
385
+ const tableItems = ref<CacheEntry[]>([])
386
+ const exportableItems = ref<CacheEntry[]>([])
387
+ const total = ref<number | null>(null)
388
+ const hasMore = ref(false)
389
+ const offset = ref(0)
390
+ const batchSizeOptions = [25, 50, 100, 200, 500]
391
+ const limit = ref(100)
392
+ const isLoadingMore = ref(false)
393
+ const detailOpen = ref(false)
394
+ const selectedEntry = ref<CacheEntry | null>(null)
395
+ const pickerKey = ref<string | null>(null)
396
+ const valueTableEntry = ref<CacheEntry | null>(null)
397
+ const valueTableItems = ref<Record<string, string>[]>([])
398
+ const valueTableAllHeaders = ref<ValueTableHeader[]>([])
399
+ const valueShowPrettyPrint = ref(false)
400
+
401
+ const {
402
+ configureDialogOpen: listConfigureDialogOpen,
403
+ disabledHeaders: listDisabledHeaders,
404
+ filterHeaders: filterListHeaders,
405
+ toggleHeader: handleToggleListHeader,
406
+ selectAll: selectAllListHeaders,
407
+ deselectAll: deselectAllListHeaders,
408
+ } = useTableColumnConfig("workbench-cache-list-hidden-columns")
409
+
410
+ const {
411
+ configureDialogOpen: valueConfigureDialogOpen,
412
+ disabledHeaders: valueDisabledHeaders,
413
+ filterHeaders: filterValueHeaders,
414
+ toggleHeader: handleToggleValueHeader,
415
+ selectAll: selectAllValueHeaders,
416
+ deselectAll: deselectAllValueHeaders,
417
+ } = useTableColumnConfig("workbench-cache-value-hidden-columns")
418
+
419
+ const handleSelectAllListHeaders = () => {
420
+ selectAllListHeaders(listAllHeaders.value)
421
+ }
422
+
423
+ const handleDeselectAllListHeaders = () => {
424
+ deselectAllListHeaders(listAllHeaders.value)
425
+ }
426
+
427
+ const handleSelectAllValueHeaders = () => {
428
+ selectAllValueHeaders(valueTableAllHeaders.value)
429
+ }
430
+
431
+ const handleDeselectAllValueHeaders = () => {
432
+ deselectAllValueHeaders(valueTableAllHeaders.value)
433
+ }
434
+
435
+ const isValueView = computed(() => valueTableEntry.value !== null)
436
+
437
+ /** Only warn when transport string was truncated and full parsed data is unavailable. */
438
+ const showValueTruncatedWarning = computed(() => {
439
+ const entry = valueTableEntry.value
440
+ if (!isValueView.value || !entry?.truncated) {
441
+ return false
442
+ }
443
+ return entry.valueParsed === undefined
444
+ })
445
+
446
+ const valueTableDataName = computed(() => {
447
+ const key = valueTableEntry.value?.key
448
+ if (!key) {
449
+ return t("workbench.cacheList.valueTableTitle")
450
+ }
451
+ return `${t("workbench.cacheList.valueTableTitle")}: ${key}`
452
+ })
453
+
454
+ const keyPickerItems = computed(() => {
455
+ const keys = new Set(tableItems.value.map((row) => row.key))
456
+ if (pickerKey.value?.trim()) {
457
+ keys.add(pickerKey.value.trim())
458
+ }
459
+ return [...keys].sort((a, b) => a.localeCompare(b))
460
+ })
461
+
462
+ const exitValueView = () => {
463
+ valueTableEntry.value = null
464
+ valueTableItems.value = []
465
+ valueTableAllHeaders.value = []
466
+ valueShowPrettyPrint.value = false
467
+ pickerKey.value = null
468
+ }
469
+
470
+ const listAllHeaders = computed(() => {
471
+ const base = [
472
+ {
473
+ title: t("workbench.cacheList.tableHeaders.key"),
474
+ key: "key",
475
+ },
476
+ {
477
+ title: t("workbench.cacheList.tableHeaders.ttl"),
478
+ key: "ttlLabel",
479
+ },
480
+ {
481
+ title: t("workbench.cacheList.tableHeaders.value"),
482
+ key: "value",
483
+ },
484
+ ]
485
+
486
+ if (!listAllKeys.value) {
487
+ return base
488
+ }
489
+
490
+ return [
491
+ {
492
+ title: t("workbench.cacheList.tableHeaders.keyGroup"),
493
+ key: "keyGroup",
494
+ },
495
+ ...base,
496
+ ]
497
+ })
498
+
499
+ const listHeaders = computed(() =>
500
+ filterListHeaders(listAllHeaders.value),
501
+ )
502
+
503
+ const visibleValueTableHeaders = computed(() =>
504
+ filterValueHeaders(valueTableAllHeaders.value),
505
+ )
506
+
507
+ const valueHeaderTitle = (key: string) => {
508
+ if (key === CACHE_KEY_COLUMN) {
509
+ return t("workbench.cacheList.tableHeaders.key")
510
+ }
511
+ if (key === "value") {
512
+ return t("workbench.cacheList.valueTableHeaders.value")
513
+ }
514
+ if (key === "entryKey") {
515
+ return t("workbench.cacheList.valueTableHeaders.entryKey")
516
+ }
517
+ if (key === "raw") {
518
+ return t("workbench.cacheList.valueTableHeaders.raw")
519
+ }
520
+ return key
521
+ }
522
+
523
+ const toTableRow = (entry: Partial<CacheEntry> & { key?: string }): CacheEntry => {
524
+ const key = entry.key ?? String(entry._id ?? "")
525
+ return {
526
+ _id: key,
527
+ key,
528
+ keyGroup: entry.keyGroup ?? "",
529
+ value: entry.value ?? "",
530
+ valuePreview: entry.valuePreview,
531
+ truncated: entry.truncated ?? false,
532
+ valueParsed: entry.valueParsed,
533
+ ttl: entry.ttl ?? -2,
534
+ ttlLabel: entry.ttlLabel ?? "",
535
+ }
536
+ }
537
+
538
+ const extractEntries = (result: ExecuteResult | undefined): CacheEntry[] => {
539
+ if (Array.isArray(result?.result) && result.result.length > 0) {
540
+ return result.result.map((row) =>
541
+ toTableRow(
542
+ (row as { _source?: CacheEntry })._source ??
543
+ (row as CacheEntry),
544
+ ),
545
+ )
546
+ }
547
+
548
+ const items = responsePayload(result)?.body?.items ?? []
549
+ return items.map((item) =>
550
+ toTableRow(item._source ?? (item as unknown as CacheEntry)),
551
+ )
552
+ }
553
+
554
+ const extractSingleEntry = (
555
+ result: ExecuteResult | undefined,
556
+ ): CacheEntry | null => {
557
+ const entries = extractEntries(result)
558
+ return entries[0] ?? null
559
+ }
560
+
561
+ const applyHits = (result: ExecuteResult | undefined) => {
562
+ const hits = responsePayload(result)?.body?.hits
563
+ if (!hits || !("hasMore" in hits)) {
564
+ return
565
+ }
566
+ total.value = hits.total ?? tableItems.value.length
567
+ hasMore.value = hits.hasMore ?? false
568
+ listAllKeys.value = hits.listAll ?? !pattern.value.trim()
569
+ activeDb.value = hits.db ?? db.value
570
+ }
571
+
572
+ const buildQueryParams = () => {
573
+ const trimmed = pattern.value.trim()
574
+ const params: Record<string, number | string> = {
575
+ db: db.value,
576
+ offset: offset.value,
577
+ limit: limit.value,
578
+ }
579
+ if (trimmed.length > 0) {
580
+ params.pattern = trimmed
581
+ }
582
+ return params
583
+ }
584
+
585
+ const fetchEntries = async (append: boolean) => {
586
+ const result = (await execute(buildQueryParams())) as ExecuteResult | undefined
587
+ const entries = extractEntries(result)
588
+ applyHits(result)
589
+
590
+ if (append) {
591
+ const existing = new Set(tableItems.value.map((row) => row._id))
592
+ const merged = [...tableItems.value]
593
+ for (const entry of entries) {
594
+ if (!existing.has(entry._id)) {
595
+ merged.push(entry)
596
+ existing.add(entry._id)
597
+ }
598
+ }
599
+ tableItems.value = merged
600
+ exportableItems.value = merged
601
+ } else {
602
+ tableItems.value = entries
603
+ exportableItems.value = entries
604
+ }
605
+ }
606
+
607
+ const reloadFromStart = async () => {
608
+ offset.value = 0
609
+ await clear()
610
+ try {
611
+ await fetchEntries(false)
612
+ } catch {
613
+ tableItems.value = []
614
+ exportableItems.value = []
615
+ total.value = null
616
+ hasMore.value = false
617
+ }
618
+ }
619
+
620
+ const loadMore = async () => {
621
+ if (!hasMore.value) return
622
+ isLoadingMore.value = true
623
+ offset.value += limit.value
624
+ try {
625
+ await fetchEntries(true)
626
+ } finally {
627
+ isLoadingMore.value = false
628
+ }
629
+ }
630
+
631
+ const displayValue = (item: CacheEntry) => {
632
+ if (item.truncated && item.valuePreview) {
633
+ return item.valuePreview
634
+ }
635
+ if (item.valueParsed !== undefined) {
636
+ return JSON.stringify(item.valueParsed)
637
+ }
638
+ return item.value
639
+ }
640
+
641
+ const formatDetailValue = (item: CacheEntry) => {
642
+ if (item.valueParsed !== undefined) {
643
+ return JSON.stringify(item.valueParsed, null, 2)
644
+ }
645
+ return item.value
646
+ }
647
+
648
+ const formatValueTableCell = (value: string | undefined) => value ?? ""
649
+
650
+ const openDetail = (item: CacheEntry) => {
651
+ selectedEntry.value = item
652
+ detailOpen.value = true
653
+ }
654
+
655
+ const openValueTableAriaLabel = (item: CacheEntry) =>
656
+ t("workbench.cacheList.openValueTableFor", { key: item.key })
657
+
658
+ const openValueTable = (item: CacheEntry) => {
659
+ void loadEntryForKey(item.key)
660
+ }
661
+
662
+ const openValueTableFromDetail = () => {
663
+ const entry = selectedEntry.value
664
+ if (!entry) {
665
+ return
666
+ }
667
+ detailOpen.value = false
668
+ void loadEntryForKey(entry.key)
669
+ }
670
+
671
+ const applyValueTable = (entry: CacheEntry) => {
672
+ const { rows, headers: shapeHeaders } = cacheEntriesValueTableRows(
673
+ [entry],
674
+ entry,
675
+ )
676
+ valueShowPrettyPrint.value = isScalarFallbackResult(rows, shapeHeaders, entry)
677
+ valueTableItems.value = rows
678
+ valueTableAllHeaders.value = shapeHeaders.map((header) => ({
679
+ key: header.key,
680
+ title: valueHeaderTitle(header.key),
681
+ }))
682
+ valueTableEntry.value = entry
683
+ pickerKey.value = entry.key
684
+ }
685
+
686
+ const stubEntryForKey = (key: string): CacheEntry => {
687
+ const listRow = tableItems.value.find((row) => row.key === key)
688
+ if (listRow) {
689
+ return listRow
690
+ }
691
+ return {
692
+ _id: key,
693
+ key,
694
+ keyGroup: "",
695
+ value: "",
696
+ truncated: false,
697
+ ttl: -2,
698
+ ttlLabel: "",
699
+ }
700
+ }
701
+
702
+ const loadEntryForKey = async (key: string) => {
703
+ const trimmed = key.trim()
704
+ if (!trimmed.length || isLoadingCacheEntry.value) {
705
+ return
706
+ }
707
+
708
+ pickerKey.value = trimmed
709
+ valueTableEntry.value = stubEntryForKey(trimmed)
710
+ valueShowPrettyPrint.value = false
711
+
712
+ try {
713
+ const result = (await fetchCacheEntry({
714
+ key: trimmed,
715
+ db: db.value,
716
+ })) as ExecuteResult | undefined
717
+ const entry = extractSingleEntry(result)
718
+ if (!entry) {
719
+ exitValueView()
720
+ return
721
+ }
722
+ applyValueTable(entry)
723
+ } catch {
724
+ exitValueView()
725
+ }
726
+ }
727
+
728
+ const onKeyPickerSelect = (key: string | null | undefined) => {
729
+ const trimmed = key?.trim() ?? ""
730
+ if (!trimmed.length) {
731
+ if (isValueView.value) {
732
+ exitValueView()
733
+ }
734
+ return
735
+ }
736
+ void loadEntryForKey(trimmed)
737
+ }
738
+
739
+ onMounted(() => {
740
+ reloadFromStart()
741
+ })
742
+ </script>