@raclettejs/workbench 0.1.34 → 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 (42) hide show
  1. package/CHANGELOG.md +18 -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 +26 -6
  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__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +1 -1
  19. package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +21 -4
  20. package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +1 -1
  21. package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +1 -1
  22. package/plugins/raclette__cache/backend/helpers/cacheEntryBuilder.ts +94 -0
  23. package/plugins/raclette__cache/backend/helpers/cacheEntrySanitizer.ts +86 -0
  24. package/plugins/raclette__cache/backend/helpers/valkeyExplorer.ts +89 -0
  25. package/plugins/raclette__cache/backend/index.ts +21 -0
  26. package/plugins/raclette__cache/backend/routes/route.cache.cacheEntries.get.ts +118 -0
  27. package/plugins/raclette__cache/backend/routes/route.cache.cacheEntry.get.ts +91 -0
  28. package/plugins/raclette__cache/backend/routes.ts +8 -0
  29. package/plugins/raclette__cache/frontend/components/TableColumnConfigDialog.vue +95 -0
  30. package/plugins/raclette__cache/frontend/composables/useTableColumnConfig.ts +66 -0
  31. package/plugins/raclette__cache/frontend/generated-config.ts +33 -0
  32. package/plugins/raclette__cache/frontend/utils/cacheValueTableRows.ts +340 -0
  33. package/plugins/raclette__cache/frontend/widgets/CacheListWidget.vue +742 -0
  34. package/plugins/raclette__cache/raclette.plugin.ts +8 -0
  35. package/services/frontend/src/app/components/dynamicForm/DynamicForm.vue +3 -1
  36. package/services/frontend/src/app/components/interactionLink/InteractionLinkBehaviorForm.vue +99 -0
  37. package/services/frontend/src/app/components/stepNavigator/StepNavigator.vue +175 -24
  38. package/services/frontend/src/app/components/stepNavigator/StepNavigatorTypes.ts +18 -0
  39. package/services/frontend/src/app/composables/useInteractionLinkBehaviorFields.ts +83 -0
  40. package/services/frontend/src/app/composables/useRacletteUserIsAdmin.ts +25 -0
  41. package/services/frontend/src/orchestrator/assets/styles/themes/dark.ts +1 -0
  42. package/services/frontend/src/orchestrator/assets/styles/themes/light.ts +1 -1
@@ -0,0 +1,89 @@
1
+ import { Redis } from "iovalkey"
2
+
3
+ export type NormalizedPattern = {
4
+ match: string
5
+ listAll: boolean
6
+ }
7
+
8
+ /** Explorer defaults to DB 0; raclette.config often sets cache db 1 for the app client. */
9
+ export const DEFAULT_EXPLORER_DB = 0
10
+
11
+ export const parseExplorerDb = (queryDb?: number): number => {
12
+ if (typeof queryDb === "number" && !Number.isNaN(queryDb) && queryDb >= 0) {
13
+ return Math.floor(queryDb)
14
+ }
15
+ return DEFAULT_EXPLORER_DB
16
+ }
17
+
18
+ export const buildRedisOptions = (db: number) => {
19
+ const raw = process.env.CACHE_URL || "redis://cache:6379"
20
+ const normalized = raw
21
+ .replace(/^valkey:/, "redis:")
22
+ .replace(/^raclette-cache:/, "redis:")
23
+
24
+ try {
25
+ const url = new URL(normalized)
26
+ return {
27
+ host: url.hostname || "cache",
28
+ port: url.port ? Number(url.port) : 6379,
29
+ db,
30
+ maxRetriesPerRequest: 1,
31
+ }
32
+ } catch {
33
+ return { host: "cache", port: 6379, db, maxRetriesPerRequest: 1 }
34
+ }
35
+ }
36
+
37
+ export const withExplorerClient = async <T>(
38
+ db: number,
39
+ fn: (client: Redis) => Promise<T>,
40
+ ): Promise<T> => {
41
+ const client = new Redis(buildRedisOptions(db))
42
+ try {
43
+ return await fn(client)
44
+ } finally {
45
+ await client.quit()
46
+ }
47
+ }
48
+
49
+ /** SCAN avoids blocking KEYS; dedupes results across cursor pages. */
50
+ export const scanKeys = async (
51
+ client: Redis,
52
+ pattern: string,
53
+ ): Promise<string[]> => {
54
+ const keys = new Set<string>()
55
+ let cursor = "0"
56
+
57
+ do {
58
+ const [next, batch] = await client.scan(
59
+ cursor,
60
+ "MATCH",
61
+ pattern,
62
+ "COUNT",
63
+ 500,
64
+ )
65
+ cursor = next
66
+ for (const key of batch) {
67
+ keys.add(key)
68
+ }
69
+ } while (cursor !== "0")
70
+
71
+ return [...keys]
72
+ }
73
+
74
+ export const normalizePattern = (pattern?: string): NormalizedPattern => {
75
+ const trimmed = pattern?.trim() ?? ""
76
+ if (!trimmed || trimmed === "*") {
77
+ return { match: "*", listAll: true }
78
+ }
79
+ return { match: trimmed, listAll: false }
80
+ }
81
+
82
+ /** Group label from key prefix (segment before first `:` or `.`). */
83
+ export const keyGroup = (key: string): string => {
84
+ const colon = key.indexOf(":")
85
+ if (colon > 0) return key.slice(0, colon)
86
+ const dot = key.indexOf(".")
87
+ if (dot > 0) return key.slice(0, dot)
88
+ return "(ungrouped)"
89
+ }
@@ -0,0 +1,21 @@
1
+ import type {
2
+ PluginFastifyInstance,
3
+ PluginOptions,
4
+ } from "@raclettejs/core/backend"
5
+ import { registerRoutes } from "./routes"
6
+
7
+ const cachePlugin = async (
8
+ fastify: PluginFastifyInstance,
9
+ _opts: PluginOptions,
10
+ ) => {
11
+ try {
12
+ await fastify.register((instance) => registerRoutes(instance))
13
+ } catch (error) {
14
+ fastify.log.error("Failed to register cache plugin routes.", error)
15
+ throw error
16
+ }
17
+
18
+ fastify.registerForFrontendGeneration({})
19
+ }
20
+
21
+ export default cachePlugin
@@ -0,0 +1,118 @@
1
+ import type { PluginFastifyInstance } from "@raclettejs/types"
2
+ import type { FastifyReply, FastifyRequest } from "fastify"
3
+ import { Type } from "@sinclair/typebox"
4
+ import {
5
+ buildCacheEntrySource,
6
+ toPayloadItems,
7
+ } from "../helpers/cacheEntryBuilder"
8
+ import {
9
+ normalizePattern,
10
+ parseExplorerDb,
11
+ scanKeys,
12
+ withExplorerClient,
13
+ } from "../helpers/valkeyExplorer"
14
+
15
+ const MAX_LIMIT = 500
16
+ const DEFAULT_LIMIT = 100
17
+
18
+ const QuerystringSchema = Type.Object({
19
+ pattern: Type.Optional(Type.String()),
20
+ offset: Type.Optional(Type.Number({ minimum: 0 })),
21
+ limit: Type.Optional(Type.Number({ minimum: 1, maximum: MAX_LIMIT })),
22
+ db: Type.Optional(Type.Number({ minimum: 0, maximum: 15 })),
23
+ })
24
+
25
+ type Querystring = {
26
+ pattern?: string
27
+ offset?: number
28
+ limit?: number
29
+ db?: number
30
+ }
31
+
32
+ export default (fastify: PluginFastifyInstance) => {
33
+ const handler = async (
34
+ req: FastifyRequest<{ Querystring: Querystring }>,
35
+ reply: FastifyReply,
36
+ ) => {
37
+ try {
38
+ const { match, listAll } = normalizePattern(req.query.pattern)
39
+ const db = parseExplorerDb(
40
+ req.query.db !== undefined ? Number(req.query.db) : undefined,
41
+ )
42
+ const offset = Math.max(0, Number(req.query.offset ?? 0))
43
+ const limit = Math.min(
44
+ MAX_LIMIT,
45
+ Math.max(1, Number(req.query.limit ?? DEFAULT_LIMIT)),
46
+ )
47
+
48
+ const entries = []
49
+ let total = 0
50
+
51
+ await withExplorerClient(db, async (client) => {
52
+ const allKeys = (await scanKeys(client, match)).sort((a, b) =>
53
+ a.localeCompare(b),
54
+ )
55
+ total = allKeys.length
56
+ const pageKeys = allKeys.slice(offset, offset + limit)
57
+
58
+ if (pageKeys.length === 0) return
59
+
60
+ const values = await client.mget(...pageKeys)
61
+
62
+ const pageEntries = await Promise.all(
63
+ pageKeys.map(async (key, index) => {
64
+ const rawValue = values[index] ?? ""
65
+ const ttl = await client.ttl(key)
66
+ return buildCacheEntrySource(key, rawValue, ttl)
67
+ }),
68
+ )
69
+
70
+ entries.push(...pageEntries)
71
+ })
72
+
73
+ const requestData = req.requestParams ?? {}
74
+
75
+ return {
76
+ appSessionId: requestData.appSessionId ?? "",
77
+ requestId: requestData.requestId ?? "",
78
+ appRequestCode: requestData.appRequestCode ?? "dataRead",
79
+ type: "dataRead",
80
+ body: {
81
+ items: toPayloadItems(entries),
82
+ hits: {
83
+ total,
84
+ offset,
85
+ limit,
86
+ hasMore: offset + limit < total,
87
+ instanceId: "default",
88
+ pattern: match,
89
+ listAll,
90
+ db,
91
+ },
92
+ },
93
+ }
94
+ } catch (err: unknown) {
95
+ const message = err instanceof Error ? err.message : String(err)
96
+ fastify.log.error(message)
97
+ return reply.internalServerError(message)
98
+ }
99
+ }
100
+
101
+ return {
102
+ handler,
103
+ onRequest: [
104
+ fastify.authenticate,
105
+ fastify.checkPermissions(["user.isAdmin"]),
106
+ ],
107
+ config: {
108
+ type: "dataRead",
109
+ },
110
+ schema: {
111
+ querystring: QuerystringSchema,
112
+ summary: "List Valkey cache entries (admin)",
113
+ description:
114
+ "Read-only SCAN of keys (optional pattern, db defaults to 0 for explorer)",
115
+ tags: ["cache"],
116
+ },
117
+ }
118
+ }
@@ -0,0 +1,91 @@
1
+ import type { PluginFastifyInstance } from "@raclettejs/types"
2
+ import type { FastifyReply, FastifyRequest } from "fastify"
3
+ import { Type } from "@sinclair/typebox"
4
+ import {
5
+ buildCacheEntrySource,
6
+ toPayloadItems,
7
+ type CacheEntrySource,
8
+ } from "../helpers/cacheEntryBuilder"
9
+ import { parseExplorerDb, withExplorerClient } from "../helpers/valkeyExplorer"
10
+
11
+ const QuerystringSchema = Type.Object({
12
+ key: Type.String({ minLength: 1 }),
13
+ db: Type.Optional(Type.Number({ minimum: 0, maximum: 15 })),
14
+ })
15
+
16
+ type Querystring = {
17
+ key: string
18
+ db?: number
19
+ }
20
+
21
+ export default (fastify: PluginFastifyInstance) => {
22
+ const handler = async (
23
+ req: FastifyRequest<{ Querystring: Querystring }>,
24
+ reply: FastifyReply,
25
+ ) => {
26
+ try {
27
+ const key = req.query.key.trim()
28
+ if (!key.length) {
29
+ return reply.badRequest("key is required")
30
+ }
31
+
32
+ const db = parseExplorerDb(
33
+ req.query.db !== undefined ? Number(req.query.db) : undefined,
34
+ )
35
+
36
+ let entry: CacheEntrySource | null = null
37
+
38
+ await withExplorerClient(db, async (client) => {
39
+ const rawValue = await client.get(key)
40
+ if (rawValue === null) {
41
+ return
42
+ }
43
+
44
+ const ttl = await client.ttl(key)
45
+ entry = buildCacheEntrySource(key, rawValue, ttl, {
46
+ attachFullParsed: true,
47
+ })
48
+ })
49
+
50
+ const requestData = req.requestParams ?? {}
51
+ const items = entry ? toPayloadItems([entry]) : []
52
+
53
+ return {
54
+ appSessionId: requestData.appSessionId ?? "",
55
+ requestId: requestData.requestId ?? "",
56
+ appRequestCode: requestData.appRequestCode ?? "dataRead",
57
+ type: "dataRead",
58
+ body: {
59
+ items,
60
+ hits: {
61
+ total: items.length,
62
+ key,
63
+ db,
64
+ found: items.length > 0,
65
+ },
66
+ },
67
+ }
68
+ } catch (err: unknown) {
69
+ const message = err instanceof Error ? err.message : String(err)
70
+ fastify.log.error(message)
71
+ return reply.internalServerError(message)
72
+ }
73
+ }
74
+
75
+ return {
76
+ handler,
77
+ onRequest: [
78
+ fastify.authenticate,
79
+ fastify.checkPermissions(["user.isAdmin"]),
80
+ ],
81
+ config: {
82
+ type: "dataRead",
83
+ },
84
+ schema: {
85
+ querystring: QuerystringSchema,
86
+ summary: "Read a single Valkey cache entry (admin)",
87
+ description: "GET + TTL for one key (no SCAN; safe for keys containing wildcards)",
88
+ tags: ["cache"],
89
+ },
90
+ }
91
+ }
@@ -0,0 +1,8 @@
1
+ import type { PluginFastifyInstance } from "@raclettejs/core/backend"
2
+ import getCacheEntriesRoute from "./routes/route.cache.cacheEntries.get"
3
+ import getCacheEntryRoute from "./routes/route.cache.cacheEntry.get"
4
+
5
+ export const registerRoutes = async (fastify: PluginFastifyInstance) => {
6
+ await fastify.get("/cacheEntries", getCacheEntriesRoute(fastify))
7
+ await fastify.get("/cacheEntry", getCacheEntryRoute(fastify))
8
+ }
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <v-dialog
3
+ v-model="isOpen"
4
+ :max-width="$vuetify.display.mobile ? undefined : '500px'"
5
+ scrollable
6
+ >
7
+ <v-card :title="$t('workbench.cacheList.configureColumns')">
8
+ <v-divider />
9
+
10
+ <v-card-text class="tw:p-0">
11
+ <div class="tw:p-4 tw:border-b tw:border-gray-200">
12
+ <div class="tw:flex tw:gap-2">
13
+ <v-btn
14
+ variant="outlined"
15
+ size="small"
16
+ prepend-icon="mdi-format-list-bulleted"
17
+ @click="emit('select-all')"
18
+ >
19
+ {{ $t("core.selectAll") }}
20
+ </v-btn>
21
+ <v-btn
22
+ variant="outlined"
23
+ size="small"
24
+ prepend-icon="mdi-format-list-checkbox"
25
+ @click="emit('deselect-all')"
26
+ >
27
+ {{ $t("workbench.cacheList.deselectAll") }}
28
+ </v-btn>
29
+ </div>
30
+ </div>
31
+
32
+ <v-list class="tw:py-0">
33
+ <v-list-item
34
+ v-for="header in headers"
35
+ :key="header.key"
36
+ class="tw:px-4"
37
+ @click="toggleHeader(header.key, !isHeaderEnabled(header.key))"
38
+ >
39
+ <template #prepend>
40
+ <v-switch
41
+ class="tw:mr-2"
42
+ :model-value="isHeaderEnabled(header.key)"
43
+ color="primary"
44
+ hide-details
45
+ inset
46
+ />
47
+ </template>
48
+ <v-list-item-title>{{ header.title }}</v-list-item-title>
49
+ </v-list-item>
50
+ </v-list>
51
+ </v-card-text>
52
+
53
+ <v-card-actions v-if="$vuetify.display.mobile">
54
+ <v-spacer />
55
+ <v-btn variant="outlined" @click="isOpen = false">
56
+ {{ $t("workbench.cacheList.done") }}
57
+ </v-btn>
58
+ </v-card-actions>
59
+ </v-card>
60
+ </v-dialog>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ import { computed } from "vue"
65
+ import type { TableColumnHeader } from "../composables/useTableColumnConfig"
66
+
67
+ const props = defineProps<{
68
+ modelValue: boolean
69
+ headers: TableColumnHeader[]
70
+ disabledHeaders: string[]
71
+ }>()
72
+
73
+ const emit = defineEmits<{
74
+ "update:modelValue": [value: boolean]
75
+ "toggle-header": [headerKey: string, enabled: boolean]
76
+ "select-all": []
77
+ "deselect-all": []
78
+ }>()
79
+
80
+ const isOpen = computed({
81
+ get: () => props.modelValue,
82
+ set: (value: boolean) => emit("update:modelValue", value),
83
+ })
84
+
85
+ const disabledHeaderKeys = computed(() =>
86
+ Array.isArray(props.disabledHeaders) ? props.disabledHeaders : [],
87
+ )
88
+
89
+ const isHeaderEnabled = (headerKey: string) =>
90
+ !disabledHeaderKeys.value.includes(headerKey)
91
+
92
+ const toggleHeader = (headerKey: string, enabled: boolean) => {
93
+ emit("toggle-header", headerKey, enabled)
94
+ }
95
+ </script>
@@ -0,0 +1,66 @@
1
+ import { ref } from "vue"
2
+ import { useLocalStorage } from "@vueuse/core"
3
+
4
+ export type TableColumnHeader = {
5
+ title: string
6
+ key: string
7
+ }
8
+
9
+ const normalizeDisabledHeaders = (value: unknown): string[] =>
10
+ Array.isArray(value) ? value.filter((key) => typeof key === "string") : []
11
+
12
+ export const useTableColumnConfig = (storageKey: string) => {
13
+ const configureDialogOpen = ref(false)
14
+ const disabledHeaders = useLocalStorage<string[]>(storageKey, [], {
15
+ mergeDefaults: (stored, defaults) =>
16
+ normalizeDisabledHeaders(stored ?? defaults),
17
+ })
18
+
19
+ if (!Array.isArray(disabledHeaders.value)) {
20
+ disabledHeaders.value = []
21
+ }
22
+
23
+ const filterHeaders = <T extends TableColumnHeader>(headers: T[]): T[] => {
24
+ const visible = headers.filter(
25
+ (header) => !disabledHeaders.value.includes(header.key),
26
+ )
27
+ return visible.length > 0 ? visible : headers
28
+ }
29
+
30
+ const toggleHeader = (headerKey: string, enabled: boolean) => {
31
+ if (enabled) {
32
+ disabledHeaders.value = disabledHeaders.value.filter(
33
+ (key) => key !== headerKey,
34
+ )
35
+ return
36
+ }
37
+ if (!disabledHeaders.value.includes(headerKey)) {
38
+ disabledHeaders.value = [...disabledHeaders.value, headerKey]
39
+ }
40
+ }
41
+
42
+ const selectAll = (headers: TableColumnHeader[]) => {
43
+ for (const header of headers) {
44
+ if (disabledHeaders.value.includes(header.key)) {
45
+ toggleHeader(header.key, true)
46
+ }
47
+ }
48
+ }
49
+
50
+ const deselectAll = (headers: TableColumnHeader[]) => {
51
+ for (const header of headers) {
52
+ if (!disabledHeaders.value.includes(header.key)) {
53
+ toggleHeader(header.key, false)
54
+ }
55
+ }
56
+ }
57
+
58
+ return {
59
+ configureDialogOpen,
60
+ disabledHeaders,
61
+ filterHeaders,
62
+ toggleHeader,
63
+ selectAll,
64
+ deselectAll,
65
+ }
66
+ }
@@ -0,0 +1,33 @@
1
+ // Auto-generated frontend configuration for plugin: raclette__cache
2
+ // This file is generated from backend routes - do not edit manually
3
+ // Entity mapping: {}
4
+
5
+ export default {
6
+ pluginName: "cache",
7
+ author: "raclette",
8
+ pluginKey: "raclette__cache",
9
+ routePrefix: "/raclette__cache",
10
+ pluginPath: "/app/src/appPlugins/raclette__cache",
11
+ data: {
12
+ cacheEntries: {
13
+ type: "cacheEntries",
14
+ operations: {
15
+ get: {
16
+ target: "/raclette__cache/cacheEntries",
17
+ method: "get",
18
+ storeActionType: "dataRead"
19
+ }
20
+ }
21
+ },
22
+ cacheEntry: {
23
+ type: "cacheEntry",
24
+ operations: {
25
+ get: {
26
+ target: "/raclette__cache/cacheEntry",
27
+ method: "get",
28
+ storeActionType: "dataRead"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }