@swiss-ai-hub/web 0.319.0 → 0.321.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 (50) hide show
  1. package/{pages/auth/login.vue → components/Auth/LoginPanel.vue} +3 -33
  2. package/components/FormKit/AgentSelector.vue +20 -7
  3. package/components/FormKit/CronInput.vue +208 -0
  4. package/components/FormKit/Repeater.vue +1 -4
  5. package/components/FormKit/VectorStoreInput.vue +43 -6
  6. package/components/Knowledge/Database/CreateModal.vue +315 -0
  7. package/components/Knowledge/Database/EmptyCard.vue +34 -0
  8. package/components/Knowledge/DeleteConfirmModal.vue +87 -0
  9. package/components/Knowledge/Document/List.vue +20 -9
  10. package/components/Knowledge/Namespace/Card.vue +18 -0
  11. package/components/Knowledge/Namespace/CreateModal.vue +11 -3
  12. package/components/Role/AccessCapabilities.vue +4 -4
  13. package/components/Role/AccessCapabilityGroup.vue +22 -6
  14. package/components/Role/AccessRulesEditor.vue +15 -12
  15. package/composables/app/useAppVersion.ts +37 -20
  16. package/composables/auth/useAuth.ts +3 -11
  17. package/composables/document/useCreateDatabase.ts +21 -0
  18. package/composables/document/useDeleteDatabase.ts +29 -0
  19. package/composables/document/useDeleteDocument.ts +1 -1
  20. package/composables/document/useDeleteDocuments.ts +1 -1
  21. package/composables/document/useDeleteNamespace.ts +30 -0
  22. package/composables/document/useIngestors.ts +23 -0
  23. package/composables/document/useScheduledDeletions.ts +16 -8
  24. package/composables/form/useCreateInstanceForm.ts +2 -2
  25. package/composables/form/useFormKitTransform.ts +44 -15
  26. package/formkit.config.ts +57 -3
  27. package/i18n/locales/de.yaml +84 -5
  28. package/i18n/locales/en.yaml +80 -3
  29. package/i18n/locales/fr.yaml +82 -3
  30. package/i18n/locales/it.yaml +84 -3
  31. package/middleware/agent-admin.ts +31 -0
  32. package/middleware/auth.global.ts +34 -16
  33. package/package.json +1 -1
  34. package/pages/[tenant]/service/agents.vue +3 -0
  35. package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id].vue +12 -5
  36. package/pages/[tenant]/service/knowledge.vue +138 -0
  37. package/pages/auth/login/[idp].vue +57 -0
  38. package/pages/auth/login/index.vue +46 -0
  39. package/plugins/0.runtime-config.client.ts +5 -0
  40. package/plugins/oidc-client.ts +1 -1
  41. package/sdk/client/client/client.gen.ts +1 -3
  42. package/sdk/client/client/types.gen.ts +7 -13
  43. package/sdk/client/client/utils.gen.ts +1 -2
  44. package/sdk/client/core/queryKeySerializer.gen.ts +1 -6
  45. package/sdk/client/core/types.gen.ts +5 -3
  46. package/sdk/client/index.ts +47 -4
  47. package/sdk/client/schemas.gen.ts +1787 -780
  48. package/sdk/client/sdk.gen.ts +231 -1
  49. package/sdk/client/types.gen.ts +1103 -226
  50. package/plugins/keycloak-client.ts +0 -41
@@ -21,36 +21,14 @@
21
21
  </a>
22
22
  <div class="flex flex-col gap-2 text-center">
23
23
  <h2 class="text-xl text-white">
24
- {{ t('auth.login.welcome', { companyName }) }}
24
+ <slot name="heading" />
25
25
  </h2>
26
26
  <p class="text-surface-400">
27
- {{ t('auth.login.pleaseLogin') }}
27
+ <slot name="message" />
28
28
  </p>
29
29
  </div>
30
30
  <div class="flex flex-col gap-3">
31
- <ProgressSpinner
32
- v-if="isLoading"
33
- class="!h-8 !w-8"
34
- />
35
- <template v-else>
36
- <Button
37
- v-for="idp in authProviders ?? []"
38
- :key="idp.alias"
39
- :label="t('auth.login.loginWith', { provider: idp.display_name })"
40
- :icon="`pi ${idp.icon}`"
41
- icon-pos="right"
42
- class="!bg-white !text-black"
43
- @click="login(idp.alias || undefined)"
44
- />
45
- <Button
46
- v-if="(authProviders?.length ?? 0) === 0"
47
- :label="t('auth.login.title')"
48
- icon="pi pi-sign-in"
49
- icon-pos="right"
50
- class="!bg-white !text-black"
51
- @click="login()"
52
- />
53
- </template>
31
+ <slot />
54
32
  </div>
55
33
  </div>
56
34
  <a
@@ -68,13 +46,5 @@
68
46
  <script setup lang="ts">
69
47
  import logo from '@core/assets/images/logo.png'
70
48
 
71
- definePageMeta({
72
- layout: 'anonymous',
73
- })
74
-
75
49
  const { t } = useI18n()
76
- const { login } = useAuth()
77
- const { authProviders, isLoading } = useAuthProviders()
78
-
79
- const companyName = 'bbv Software Services AG'
80
50
  </script>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="flex flex-col gap-4">
3
- <!-- Agent Class Selection -->
4
- <div>
3
+ <!-- Agent Class Selection (omitted when the config pins the class) -->
4
+ <div v-if="!pinnedClass">
5
5
  <label
6
6
  for="agent-class-select"
7
7
  class="mb-1 block text-sm font-medium"
@@ -139,6 +139,7 @@ interface AgentSelectorProps {
139
139
  value?: AgentSelectorValue | null
140
140
  attrs: Record<string, unknown>
141
141
  startEvent?: string
142
+ agentClass?: string
142
143
  classPlaceholder?: string
143
144
  idPlaceholder?: string
144
145
  filter?: boolean
@@ -151,6 +152,9 @@ const { tenantId } = useTenant()
151
152
 
152
153
  // Get custom props from context (FormKit passes them there, not as direct props)
153
154
  const startEvent = computed(() => props.context.startEvent)
155
+ // Set by configs that already know which blueprint answers: the class dropdown is not rendered and only
156
+ // this class's profiles are offered.
157
+ const pinnedClass = computed(() => props.context.agentClass)
154
158
  const classPlaceholder = computed(() => props.context.classPlaceholder)
155
159
  const idPlaceholder = computed(() => props.context.idPlaceholder)
156
160
  const filter = computed(() => props.context.filter ?? true)
@@ -166,7 +170,7 @@ const currentValue = computed(() => props.context.value ?? null)
166
170
 
167
171
  // Selected class (computed property that syncs with the form value)
168
172
  const selectedClass = computed({
169
- get: () => currentValue.value?.agent_class ?? null,
173
+ get: () => currentValue.value?.agent_class ?? pinnedClass.value ?? null,
170
174
  set: (value: string | null) => {
171
175
  if (value) {
172
176
  // When class changes, reset agent ID and fetch instances
@@ -290,16 +294,25 @@ async function fetchInstances(agentClass: string) {
290
294
 
291
295
  // Fetch classes on mount
292
296
  onMounted(() => {
293
- fetchClasses()
297
+ // A pinned class needs no class list — nothing renders it, and the profile dropdown is driven by
298
+ // fetchInstances alone.
299
+ if (!pinnedClass.value) {
300
+ fetchClasses()
301
+ }
294
302
 
295
- // If there's already a selected class, fetch its instances
296
- if (currentValue.value?.agent_class) {
297
- fetchInstances(currentValue.value.agent_class)
303
+ // Pinned, or already chosen: either way the profile dropdown needs that class's instances up front.
304
+ const classToLoad = currentValue.value?.agent_class ?? pinnedClass.value
305
+ if (classToLoad) {
306
+ fetchInstances(classToLoad)
298
307
  }
299
308
  })
300
309
 
301
310
  // Refetch if startEvent changes
302
311
  watch(startEvent, () => {
312
+ // A pinned class is not filtered by start event, and its class list is never fetched — so the check below
313
+ // would find no match and clear a perfectly valid selection.
314
+ if (pinnedClass.value) return
315
+
303
316
  // Clear selection if current class no longer matches filter
304
317
  if (selectedClass.value && !filteredClassOptions.value.some(opt => opt.name === selectedClass.value)) {
305
318
  props.context.node.input(null)
@@ -0,0 +1,208 @@
1
+ <template>
2
+ <div class="flex flex-col gap-4">
3
+ <div class="grid grid-cols-2 gap-3 sm:grid-cols-5">
4
+ <div
5
+ v-for="position in POSITIONS"
6
+ :key="position.key"
7
+ >
8
+ <label
9
+ :for="`cron-${position.key}`"
10
+ class="mb-1 block text-sm font-medium"
11
+ >
12
+ {{ t(`lib.cron.${position.key}.label`) }}
13
+ </label>
14
+ <InputText
15
+ :id="`cron-${position.key}`"
16
+ :model-value="schedule[position.key]"
17
+ :aria-label="t(`lib.cron.${position.key}.label`)"
18
+ :invalid="Boolean(errors[position.key])"
19
+ :aria-invalid="Boolean(errors[position.key])"
20
+ class="w-full font-mono"
21
+ @update:model-value="(value) => updatePosition(position.key, value ?? '')"
22
+ />
23
+ <small
24
+ v-if="errors[position.key]"
25
+ class="mt-1 block text-red-500"
26
+ >
27
+ {{ errors[position.key] }}
28
+ </small>
29
+ <small
30
+ v-else
31
+ class="mt-1 block text-surface-500"
32
+ >{{ position.range }}</small>
33
+ </div>
34
+ </div>
35
+
36
+ <div>
37
+ <label
38
+ for="cron-timezone"
39
+ class="mb-1 block text-sm font-medium"
40
+ >
41
+ {{ t('lib.cron.timezone.label') }}
42
+ </label>
43
+ <Select
44
+ v-model="timezone"
45
+ :aria-label="t('lib.cron.timezone.label')"
46
+ input-id="cron-timezone"
47
+ :options="timezoneOptions"
48
+ :placeholder="timezonePlaceholder ?? t('lib.cron.timezone.placeholder')"
49
+ :filter="filter"
50
+ class="w-full"
51
+ />
52
+ <small class="mt-1 block text-surface-500">{{ t('lib.cron.timezone.help') }}</small>
53
+ </div>
54
+
55
+ <small class="text-surface-500">
56
+ {{ t('lib.cron.expression') }} <code class="font-mono">{{ expression }}</code>
57
+ </small>
58
+ </div>
59
+ </template>
60
+
61
+ <script setup lang="ts">
62
+ /**
63
+ * Editor for a backend `CronSchedule`: the five cron positions plus an IANA timezone.
64
+ *
65
+ * The emitted value is the schedule object in the backend's own field names, so it round-trips through
66
+ * `config_data` untouched — the scheduler reads those keys directly. Positions are kept separate rather than
67
+ * joined into one "0 12 * * *" string because that is how `CronSchedule` stores them, and flattening here would
68
+ * make every consumer parse it back apart.
69
+ *
70
+ * Deliberately has no presets and no plain-language summary — that is #1581, which extends this component rather
71
+ * than replacing it.
72
+ */
73
+
74
+ import { createMessage } from '@formkit/core'
75
+
76
+ import type { FormKitNode } from '@formkit/core'
77
+
78
+ interface CronSchedule {
79
+ minute: string
80
+ hour: string
81
+ day_of_month: string
82
+ month: string
83
+ day_of_week: string
84
+ timezone: string
85
+ }
86
+
87
+ type CronPosition = Exclude<keyof CronSchedule, 'timezone'>
88
+
89
+ interface CronInputProps {
90
+ context: {
91
+ node: FormKitNode
92
+ value?: CronSchedule | null
93
+ attrs: Record<string, unknown>
94
+ timezonePlaceholder?: string
95
+ filter?: boolean
96
+ }
97
+ }
98
+
99
+ const props = defineProps<CronInputProps>()
100
+ const { t } = useI18n()
101
+
102
+ const POSITIONS: { key: CronPosition, range: string, min: number, max: number }[] = [
103
+ { key: 'minute', range: '0-59', min: 0, max: 59 },
104
+ { key: 'hour', range: '0-23', min: 0, max: 23 },
105
+ { key: 'day_of_month', range: '1-31', min: 1, max: 31 },
106
+ { key: 'month', range: '1-12', min: 1, max: 12 },
107
+ { key: 'day_of_week', range: '0-6', min: 0, max: 6 },
108
+ ]
109
+
110
+ // Deliberately permissive: croniter accepts names (MON-FRI, JAN), steps, lists and the day-of-week specials, and a
111
+ // stricter pattern here would reject schedules the backend is happy to run. This catches the two things that are
112
+ // wrong in every cron dialect — a blank position and a stray character — and leaves grammar to the server, which
113
+ // now rejects a malformed cron on save rather than storing it.
114
+ const ALLOWED_CHARACTERS = /^[0-9A-Za-z*,\-/?#]+$/
115
+
116
+ // Blocking so the surrounding form cannot be submitted while a position is empty or malformed. Without it the only
117
+ // feedback is a 400 from the save endpoint, and before that endpoint validated, a blank position was stored and
118
+ // silently broke every run of the profile — not only its scheduled ones.
119
+ const ERROR_MESSAGE_KEY = 'cronPositions'
120
+
121
+ // Shown in the inputs the moment the field is enabled, never applied behind the user's back. `CronSchedule`
122
+ // requires all five positions and has no defaults, so leaving them blank would save a schedule the scheduler then
123
+ // skips with only a log line to say why. Hourly is the least surprising visible starting point — `* * * * *` would
124
+ // commit an admin to a run every minute for merely ticking the enable box.
125
+ const HOURLY: CronSchedule = {
126
+ minute: '0',
127
+ hour: '*',
128
+ day_of_month: '*',
129
+ month: '*',
130
+ day_of_week: '*',
131
+ timezone: resolveBrowserTimezone(),
132
+ }
133
+
134
+ const timezonePlaceholder = computed(() => props.context.timezonePlaceholder)
135
+ const filter = computed(() => props.context.filter ?? true)
136
+
137
+ const schedule = computed<CronSchedule>(() => props.context.value ?? HOURLY)
138
+
139
+ const errors = computed<Partial<Record<CronPosition, string>>>(() =>
140
+ Object.fromEntries(
141
+ POSITIONS.map(position => [position.key, positionError(position)]).filter(([, message]) => message),
142
+ ),
143
+ )
144
+
145
+ const expression = computed(() =>
146
+ POSITIONS.map(position => schedule.value[position.key] || '?').join(' '),
147
+ )
148
+
149
+ const timezone = computed({
150
+ get: () => schedule.value.timezone,
151
+ set: (value: string) => emitValue({ ...schedule.value, timezone: value }),
152
+ })
153
+
154
+ const timezoneOptions = computed<string[]>(() => {
155
+ // Not every engine implements supportedValuesOf; without it the admin can still keep the resolved zone.
156
+ const supported = (Intl as { supportedValuesOf?: (key: string) => string[] }).supportedValuesOf
157
+ return supported ? supported('timeZone') : [schedule.value.timezone, 'UTC']
158
+ })
159
+
160
+ function resolveBrowserTimezone(): string {
161
+ try {
162
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
163
+ }
164
+ catch {
165
+ return 'UTC'
166
+ }
167
+ }
168
+
169
+ function positionError(position: { key: CronPosition, min: number, max: number }): string | undefined {
170
+ const value = schedule.value[position.key]
171
+ if (!value) return t('lib.cron.errors.required')
172
+ if (!ALLOWED_CHARACTERS.test(value)) return t('lib.cron.errors.invalid')
173
+
174
+ // Only plain integers are range-checked. Anything with a step, range or name is left to the server, whose
175
+ // croniter knows the dialect far better than a regex here could.
176
+ const outOfRange = value
177
+ .split(',')
178
+ .some(term => /^\d+$/.test(term) && (Number(term) < position.min || Number(term) > position.max))
179
+
180
+ return outOfRange ? t('lib.cron.errors.range', { min: position.min, max: position.max }) : undefined
181
+ }
182
+
183
+ function updatePosition(position: CronPosition, value: string) {
184
+ emitValue({ ...schedule.value, [position]: value.trim() })
185
+ }
186
+
187
+ // Always emits every key: a partial schedule passes the generated submission schema (which cannot carry
188
+ // CronSchedule's croniter and timezone validators) and is then rejected by the save endpoint.
189
+ function emitValue(value: CronSchedule) {
190
+ props.context.node.input({ ...value })
191
+ }
192
+
193
+ watch(errors, (current) => {
194
+ props.context.node.store.remove(ERROR_MESSAGE_KEY)
195
+ const first = Object.values(current)[0]
196
+ if (!first) return
197
+
198
+ props.context.node.store.set(
199
+ createMessage({ blocking: true, key: ERROR_MESSAGE_KEY, type: 'error', value: first, visible: true }),
200
+ )
201
+ }, { immediate: true })
202
+
203
+ // Seed the stored value so enabling the field yields a complete, valid schedule even if the admin saves without
204
+ // touching an input — what the inputs show and what gets saved must not diverge.
205
+ onMounted(() => {
206
+ if (!props.context.value) emitValue(HOURLY)
207
+ })
208
+ </script>
@@ -99,11 +99,8 @@ const isRemoveDisabled = computed(() => {
99
99
 
100
100
  function addItem() {
101
101
  if (isAddDisabled.value) return
102
- if (!modelValue.value) {
103
- modelValue.value = []
104
- }
105
102
  rowKeys.value.push(makeRowKey())
106
- modelValue.value.push(props.defaultItem ? cloneDeep(props.defaultItem) : {})
103
+ modelValue.value = [...items.value, props.defaultItem ? cloneDeep(props.defaultItem) : {}]
107
104
  }
108
105
 
109
106
  function removeItem(index: number) {
@@ -44,8 +44,28 @@
44
44
  </Select>
45
45
  </div>
46
46
 
47
- <!-- Namespace Selection (shown when database selected) -->
48
- <div v-if="selectedDatabase">
47
+ <!-- Namespace scope (shown when database selected) -->
48
+ <div
49
+ v-if="selectedDatabase"
50
+ class="flex items-center gap-2"
51
+ >
52
+ <ToggleSwitch
53
+ v-model="allNamespaces"
54
+ input-id="vector-store-all-namespaces"
55
+ />
56
+ <label
57
+ for="vector-store-all-namespaces"
58
+ class="text-sm"
59
+ >{{ t('lib.vectorStore.allNamespaces.label') }}</label>
60
+ </div>
61
+ <small
62
+ v-if="selectedDatabase && allNamespaces"
63
+ class="-mt-3 text-surface-500"
64
+ >
65
+ {{ t('lib.vectorStore.allNamespaces.help', { count: namespaceOptions.length }) }}
66
+ </small>
67
+
68
+ <div v-if="selectedDatabase && !allNamespaces">
49
69
  <label
50
70
  for="vector-store-namespaces-select"
51
71
  class="mb-1 block text-sm font-medium"
@@ -104,6 +124,7 @@ import type { DatabaseDto } from '@core/sdk/client'
104
124
  interface VectorStoreInputValue {
105
125
  collection_name: string
106
126
  index_namespaces: string[]
127
+ all_namespaces: boolean
107
128
  allowed_metadata_filter_fields: string[]
108
129
  }
109
130
 
@@ -154,7 +175,7 @@ const selectedDatabase = computed({
154
175
  set: (value: string | null) => {
155
176
  if (value) {
156
177
  // When database changes, reset namespaces and allowed filter fields
157
- emitValue(value, [], [])
178
+ emitValue(value, [], false, [])
158
179
  }
159
180
  else {
160
181
  props.context.node.input(null)
@@ -162,12 +183,22 @@ const selectedDatabase = computed({
162
183
  },
163
184
  })
164
185
 
186
+ // Whole-database scope: switching it on discards any named namespaces so the two never coexist
187
+ const allNamespaces = computed({
188
+ get: () => currentValue.value?.all_namespaces ?? false,
189
+ set: (value: boolean) => {
190
+ if (selectedDatabase.value) {
191
+ emitValue(selectedDatabase.value, value ? [] : selectedNamespaces.value, value, allowedFilterFields.value)
192
+ }
193
+ },
194
+ })
195
+
165
196
  // Selected namespaces (computed property that syncs with the form value)
166
197
  const selectedNamespaces = computed({
167
198
  get: () => currentValue.value?.index_namespaces ?? [],
168
199
  set: (value: string[]) => {
169
200
  if (selectedDatabase.value) {
170
- emitValue(selectedDatabase.value, value, allowedFilterFields.value)
201
+ emitValue(selectedDatabase.value, value, false, allowedFilterFields.value)
171
202
  }
172
203
  },
173
204
  })
@@ -179,7 +210,7 @@ const chipsInputContext = computed(() => ({
179
210
  node: {
180
211
  input: (value: string[]) => {
181
212
  if (selectedDatabase.value) {
182
- emitValue(selectedDatabase.value, selectedNamespaces.value, value)
213
+ emitValue(selectedDatabase.value, selectedNamespaces.value, allNamespaces.value, value)
183
214
  }
184
215
  },
185
216
  },
@@ -189,10 +220,16 @@ const chipsInputContext = computed(() => ({
189
220
  }))
190
221
 
191
222
  // Emit complete value object
192
- function emitValue(collectionName: string, namespaces: string[], allowedFilterFieldKeys: string[]) {
223
+ function emitValue(
224
+ collectionName: string,
225
+ namespaces: string[],
226
+ wholeDatabase: boolean,
227
+ allowedFilterFieldKeys: string[],
228
+ ) {
193
229
  props.context.node.input({
194
230
  collection_name: collectionName,
195
231
  index_namespaces: namespaces,
232
+ all_namespaces: wholeDatabase,
196
233
  allowed_metadata_filter_fields: allowedFilterFieldKeys,
197
234
  })
198
235
  }