@swiss-ai-hub/web 0.320.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 (38) hide show
  1. package/components/FormKit/AgentSelector.vue +20 -7
  2. package/components/FormKit/Repeater.vue +1 -4
  3. package/components/FormKit/VectorStoreInput.vue +43 -6
  4. package/components/Knowledge/Database/CreateModal.vue +315 -0
  5. package/components/Knowledge/Database/EmptyCard.vue +34 -0
  6. package/components/Knowledge/DeleteConfirmModal.vue +87 -0
  7. package/components/Knowledge/Namespace/Card.vue +18 -0
  8. package/components/Knowledge/Namespace/CreateModal.vue +11 -3
  9. package/components/Role/AccessCapabilities.vue +4 -4
  10. package/components/Role/AccessCapabilityGroup.vue +22 -6
  11. package/components/Role/AccessRulesEditor.vue +15 -12
  12. package/composables/app/useAppVersion.ts +31 -33
  13. package/composables/auth/useAuth.ts +3 -11
  14. package/composables/document/useCreateDatabase.ts +21 -0
  15. package/composables/document/useDeleteDatabase.ts +29 -0
  16. package/composables/document/useDeleteDocument.ts +1 -1
  17. package/composables/document/useDeleteDocuments.ts +1 -1
  18. package/composables/document/useDeleteNamespace.ts +30 -0
  19. package/composables/document/useIngestors.ts +23 -0
  20. package/composables/form/useCreateInstanceForm.ts +2 -2
  21. package/composables/form/useFormKitTransform.ts +44 -15
  22. package/formkit.config.ts +24 -1
  23. package/i18n/locales/de.yaml +60 -5
  24. package/i18n/locales/en.yaml +57 -3
  25. package/i18n/locales/fr.yaml +58 -3
  26. package/i18n/locales/it.yaml +60 -3
  27. package/middleware/agent-admin.ts +31 -0
  28. package/package.json +1 -1
  29. package/pages/[tenant]/service/agents.vue +3 -0
  30. package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id].vue +12 -5
  31. package/pages/[tenant]/service/knowledge.vue +138 -0
  32. package/plugins/0.runtime-config.client.ts +5 -0
  33. package/plugins/oidc-client.ts +1 -1
  34. package/sdk/client/index.ts +29 -2
  35. package/sdk/client/schemas.gen.ts +641 -77
  36. package/sdk/client/sdk.gen.ts +159 -1
  37. package/sdk/client/types.gen.ts +543 -75
  38. package/plugins/keycloak-client.ts +0 -41
@@ -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)
@@ -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
  }
@@ -0,0 +1,315 @@
1
+ <template>
2
+ <Dialog
3
+ v-model:visible="visible"
4
+ modal
5
+ :header="t('knowledge.form.create_database.title')"
6
+ :style="{ width: '50rem' }"
7
+ :closable="!isSubmitting"
8
+ >
9
+ <div class="flex flex-col gap-6">
10
+ <div
11
+ v-if="ingestorsAreLoading"
12
+ class="flex items-center justify-center py-8"
13
+ >
14
+ <ProgressSpinner />
15
+ </div>
16
+
17
+ <div
18
+ v-else-if="!ingestors || ingestors.length === 0"
19
+ class="py-8 text-center text-surface-500"
20
+ >
21
+ {{ t('knowledge.form.create_database.no_ingestors') }}
22
+ </div>
23
+
24
+ <template v-else>
25
+ <div class="flex flex-col gap-2">
26
+ <label
27
+ for="database-name-input"
28
+ class="text-sm font-medium"
29
+ >
30
+ {{ t('knowledge.form.database_name.label') }}
31
+ <span class="ml-1 text-xs text-red-500">*</span>
32
+ </label>
33
+ <InputText
34
+ id="database-name-input"
35
+ v-model="databaseName"
36
+ :placeholder="t('knowledge.form.database_name.placeholder')"
37
+ :class="{ 'p-invalid': nameValidationError }"
38
+ :disabled="isSubmitting"
39
+ />
40
+ <small
41
+ v-if="nameValidationError"
42
+ class="text-red-500"
43
+ >{{ nameValidationError }}</small>
44
+ <small
45
+ v-else
46
+ class="text-gray-500"
47
+ >{{ t('knowledge.form.database_name.help') }}</small>
48
+ </div>
49
+
50
+ <div class="flex flex-col gap-2">
51
+ <label
52
+ for="database-ingestor-select"
53
+ class="text-sm font-medium"
54
+ >
55
+ {{ t('knowledge.form.ingestor.label') }}
56
+ <span class="ml-1 text-xs text-red-500">*</span>
57
+ </label>
58
+ <Select
59
+ id="database-ingestor-select"
60
+ v-model="selectedClass"
61
+ :options="ingestors"
62
+ option-label="display_name"
63
+ option-value="name"
64
+ :placeholder="t('knowledge.form.ingestor.placeholder')"
65
+ :disabled="isSubmitting"
66
+ class="w-full"
67
+ />
68
+ <small class="text-gray-500">
69
+ {{ selectedClassData?.description || t('knowledge.form.ingestor.help') }}
70
+ </small>
71
+ </div>
72
+
73
+ <div
74
+ v-if="selectedClassData && configForm.length > 0 && formReady"
75
+ class="content flex flex-col gap-2"
76
+ >
77
+ <FormKit
78
+ id="create-database-form"
79
+ v-model="formData"
80
+ type="form"
81
+ :actions="false"
82
+ :config="{
83
+ validationVisibility: 'dirty',
84
+ }"
85
+ @submit="handleFormSubmit"
86
+ @submit-invalid="isSubmitting = false"
87
+ >
88
+ <Stepper
89
+ v-model:value="activeStep"
90
+ orientation="vertical"
91
+ >
92
+ <StepItem
93
+ v-if="simpleElementsSchema.length > 0"
94
+ :value="0"
95
+ >
96
+ <Step>{{ t('knowledge.form.create_database.steps.basic_info') }}</Step>
97
+ <StepPanel>
98
+ <div class="flex flex-col gap-6 py-4">
99
+ <FormKitSchema
100
+ :schema="simpleElementsSchema"
101
+ :data="formData"
102
+ />
103
+ </div>
104
+ </StepPanel>
105
+ </StepItem>
106
+ <StepItem
107
+ v-for="(group, index) in groupConfigs"
108
+ :key="`group-${group.name}`"
109
+ :value="getGroupStepIndex(index)"
110
+ >
111
+ <Step>{{ group.label || group.name }}</Step>
112
+ <StepPanel>
113
+ <div class="content py-4">
114
+ <FormKitSchema
115
+ :schema="group.schema"
116
+ :data="formData"
117
+ />
118
+ </div>
119
+ </StepPanel>
120
+ </StepItem>
121
+ <StepItem
122
+ v-for="(rep, index) in repeaterConfigs"
123
+ :key="`repeater-${rep.path}`"
124
+ :value="getRepeaterStepIndex(index)"
125
+ >
126
+ <Step>{{ rep.label || rep.name }}</Step>
127
+ <StepPanel>
128
+ <div class="py-4">
129
+ <FormKitRepeater
130
+ :model-value="getRepeaterData(rep.path)"
131
+ :name="rep.name"
132
+ :label="rep.label"
133
+ :add-label="rep.addLabel"
134
+ :children-schema="rep.childrenSchema"
135
+ :default-item="rep.defaultItem"
136
+ :min="rep.min"
137
+ :max="rep.max"
138
+ @update:model-value="setRepeaterData(rep.path, $event)"
139
+ />
140
+ </div>
141
+ </StepPanel>
142
+ </StepItem>
143
+ </Stepper>
144
+ </FormKit>
145
+ </div>
146
+ </template>
147
+ </div>
148
+
149
+ <template #footer>
150
+ <Button
151
+ :label="t('knowledge.actions.cancel')"
152
+ severity="secondary"
153
+ outlined
154
+ @click="closeModal"
155
+ />
156
+ <Button
157
+ :label="t('knowledge.actions.create')"
158
+ :disabled="!canSubmit"
159
+ :loading="isSubmitting"
160
+ @click="triggerFormSubmit"
161
+ />
162
+ </template>
163
+ </Dialog>
164
+ </template>
165
+
166
+ <script setup lang="ts">
167
+ import { type FormElement, serializeFormData } from '@core/composables/form/useFormKitTransform'
168
+ import { getNode } from '@formkit/core'
169
+
170
+ const props = defineProps<{
171
+ modelValue: boolean
172
+ }>()
173
+
174
+ const emit = defineEmits<{
175
+ 'update:modelValue': [value: boolean]
176
+ 'success': [data: { database: string }]
177
+ }>()
178
+
179
+ const { t, locale } = useI18n()
180
+ const toast = useToast()
181
+ const { mutateAsync: createDatabase } = useCreateDatabase()
182
+ const { ingestors, ingestorsAreLoading } = useIngestors()
183
+ const { tenantId } = useTenant()
184
+
185
+ // The ingestor list is the "class" list: each ingestor announces the form its databases are configured
186
+ // through, exactly as an agent class does, so the agent create flow is reused as is.
187
+ const {
188
+ selectedClass,
189
+ formData,
190
+ activeStep,
191
+ selectedClassData,
192
+ configForm,
193
+ simpleElementsSchema,
194
+ groupConfigs,
195
+ repeaterConfigs,
196
+ getGroupStepIndex,
197
+ getRepeaterStepIndex,
198
+ getRepeaterData,
199
+ setRepeaterData,
200
+ resetForm,
201
+ } = useCreateInstanceForm({
202
+ classes: ingestors,
203
+ classField: 'name',
204
+ idField: 'name',
205
+ initialClass: () => ingestors.value?.[0]?.name ?? '',
206
+ locale,
207
+ })
208
+
209
+ const databaseName = ref('')
210
+ const isSubmitting = ref(false)
211
+ const formReady = ref(false)
212
+
213
+ const visible = computed({
214
+ get: () => props.modelValue,
215
+ set: (value: boolean) => emit('update:modelValue', value),
216
+ })
217
+
218
+ watch(visible, async (isVisible) => {
219
+ if (!isVisible) {
220
+ formReady.value = false
221
+ databaseName.value = ''
222
+ resetForm()
223
+ return
224
+ }
225
+ await nextTick()
226
+ formReady.value = true
227
+ })
228
+
229
+ const nameValidationError = computed(() => {
230
+ if (!databaseName.value.trim()) return ''
231
+
232
+ const namePattern = /^[a-z][a-z0-9]{2,62}$/
233
+ if (!namePattern.test(databaseName.value)) {
234
+ return t('knowledge.form.database_name.validation_error')
235
+ }
236
+ return ''
237
+ })
238
+
239
+ const canSubmit = computed(
240
+ () => databaseName.value.trim() && selectedClass.value && !nameValidationError.value && !isSubmitting.value,
241
+ )
242
+
243
+ function closeModal() {
244
+ visible.value = false
245
+ }
246
+
247
+ function triggerFormSubmit() {
248
+ if (!canSubmit.value) return
249
+ isSubmitting.value = true
250
+ const formNode = getNode('create-database-form')
251
+ if (formNode) {
252
+ formNode.submit()
253
+ }
254
+ else {
255
+ handleFormSubmit()
256
+ }
257
+ }
258
+
259
+ async function handleFormSubmit() {
260
+ const database = databaseName.value
261
+ try {
262
+ await createDatabase({
263
+ database,
264
+ tenantId: tenantId.value!,
265
+ request: {
266
+ ingestor: selectedClass.value,
267
+ configuration: serializeFormData(formData.value, configForm.value as FormElement[]),
268
+ },
269
+ })
270
+
271
+ toast.add({
272
+ severity: 'success',
273
+ summary: t('knowledge.form.create_database.success'),
274
+ life: 3000,
275
+ })
276
+
277
+ emit('success', { database })
278
+ closeModal()
279
+ }
280
+ catch (error) {
281
+ console.error('Failed to create knowledge database:', error)
282
+ toast.add({
283
+ severity: 'error',
284
+ summary: t('knowledge.form.create_database.error'),
285
+ detail: error instanceof Error ? error.message : String(error),
286
+ life: 5000,
287
+ })
288
+ }
289
+ finally {
290
+ isSubmitting.value = false
291
+ }
292
+ }
293
+ </script>
294
+
295
+ <style scoped>
296
+ .content {
297
+ @apply font-light text-xs
298
+ }
299
+
300
+ .content :deep(.formkit-group-fieldset) {
301
+ @apply flex flex-col gap-6;
302
+ }
303
+
304
+ .content :deep(.formkit-outer) {
305
+ @apply pt-3 pb-1;
306
+ }
307
+
308
+ .content :deep(.formkit-group-fieldset legend) {
309
+ @apply hidden;
310
+ }
311
+
312
+ .content :deep(#create-database-form-incomplete) {
313
+ @apply font-bold text-sm text-right pr-2;
314
+ }
315
+ </style>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ class="flex w-full cursor-pointer items-center gap-3 rounded-xl border-2 border-dashed border-surface-300 p-4 text-left hover:border-primary-500 hover:bg-surface-50 dark:border-surface-600 dark:hover:bg-surface-800"
5
+ @click="handleAdd"
6
+ >
7
+ <div class="flex items-center justify-center p-2">
8
+ <i
9
+ class="pi pi-database text-surface-400"
10
+ style="font-size: 1.5rem"
11
+ />
12
+ </div>
13
+ <div>
14
+ <h3 class="font-medium text-surface-600 dark:text-surface-400">
15
+ {{ t('knowledge.add_database.title') }}
16
+ </h3>
17
+ <p class="text-sm text-surface-500 dark:text-surface-400">
18
+ {{ t('knowledge.add_database.description') }}
19
+ </p>
20
+ </div>
21
+ </button>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ const { t } = useI18n()
26
+
27
+ const emit = defineEmits<{
28
+ add: []
29
+ }>()
30
+
31
+ const handleAdd = () => {
32
+ emit('add')
33
+ }
34
+ </script>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <Dialog
3
+ :visible="visible"
4
+ modal
5
+ :header="title"
6
+ :style="{ width: '32rem' }"
7
+ :closable="!isDeleting"
8
+ @update:visible="emit('update:visible', $event)"
9
+ >
10
+ <div class="flex flex-col gap-4">
11
+ <Message
12
+ severity="warn"
13
+ :closable="false"
14
+ >
15
+ {{ warning }}
16
+ </Message>
17
+
18
+ <div class="flex flex-col gap-2">
19
+ <label
20
+ for="delete-confirm-input"
21
+ class="text-sm font-medium"
22
+ >
23
+ {{ t('knowledge.delete.confirm_label', { name: expectedName }) }}
24
+ </label>
25
+ <!-- Deliberately no placeholder: the label already names what to type, and putting it in the
26
+ field too turns a deliberate confirmation into copying the answer out of the box. -->
27
+ <InputText
28
+ id="delete-confirm-input"
29
+ v-model="typedName"
30
+ autocomplete="off"
31
+ :disabled="isDeleting"
32
+ @keyup.enter="confirmIfMatch"
33
+ />
34
+ </div>
35
+ </div>
36
+
37
+ <template #footer>
38
+ <div class="flex justify-end gap-2">
39
+ <Button
40
+ :label="t('knowledge.actions.cancel')"
41
+ severity="secondary"
42
+ outlined
43
+ :disabled="isDeleting"
44
+ @click="emit('update:visible', false)"
45
+ />
46
+ <Button
47
+ :label="t('knowledge.delete.actions.delete')"
48
+ severity="danger"
49
+ :disabled="!nameMatches"
50
+ :loading="isDeleting"
51
+ @click="emit('confirm')"
52
+ />
53
+ </div>
54
+ </template>
55
+ </Dialog>
56
+ </template>
57
+
58
+ <script setup lang="ts">
59
+ const props = defineProps<{
60
+ visible: boolean
61
+ title: string
62
+ warning: string
63
+ expectedName: string
64
+ isDeleting?: boolean
65
+ }>()
66
+
67
+ const emit = defineEmits<{
68
+ 'update:visible': [value: boolean]
69
+ 'confirm': []
70
+ }>()
71
+
72
+ const { t } = useI18n()
73
+
74
+ const typedName = ref('')
75
+
76
+ const nameMatches = computed(() => typedName.value.trim() === props.expectedName)
77
+
78
+ const confirmIfMatch = () => {
79
+ if (nameMatches.value && !props.isDeleting) emit('confirm')
80
+ }
81
+
82
+ // Reset the typed name whenever the dialog opens for a different target, so a previous confirmation
83
+ // can never carry over and pre-satisfy the guard.
84
+ watch(() => props.visible, (isVisible) => {
85
+ if (isVisible) typedName.value = ''
86
+ })
87
+ </script>
@@ -45,6 +45,16 @@
45
45
  severity="secondary"
46
46
  @click.stop="handleUploadClick"
47
47
  />
48
+ <Button
49
+ v-if="!autoSync"
50
+ v-tooltip.top="t('knowledge.delete_namespace')"
51
+ icon="pi pi-trash"
52
+ rounded
53
+ text
54
+ size="small"
55
+ severity="danger"
56
+ @click.stop="handleDeleteClick"
57
+ />
48
58
  <Badge
49
59
  :value="namespace.number_of_documents"
50
60
  size="large"
@@ -67,6 +77,8 @@ import { capitalCase } from 'change-case'
67
77
 
68
78
  import type { NamespaceDto } from '@core/sdk/client'
69
79
 
80
+ // Not gated on the database's `deletable`: a legacy database cannot be removed as a whole, but its
81
+ // namespaces can. Only an auto-synced source, which would just re-sync them, blocks both affordances.
70
82
  const props = defineProps<{
71
83
  namespace: NamespaceDto
72
84
  autoSync?: boolean
@@ -75,6 +87,7 @@ const props = defineProps<{
75
87
  const emit = defineEmits<{
76
88
  upload: [namespace: NamespaceDto]
77
89
  edit: [namespace: NamespaceDto]
90
+ delete: [namespace: NamespaceDto]
78
91
  }>()
79
92
 
80
93
  const route = useRoute()
@@ -105,4 +118,9 @@ const handleEditClick = (event: Event) => {
105
118
  event.stopPropagation()
106
119
  emit('edit', props.namespace)
107
120
  }
121
+
122
+ const handleDeleteClick = (event: Event) => {
123
+ event.stopPropagation()
124
+ emit('delete', props.namespace)
125
+ }
108
126
  </script>
@@ -190,9 +190,17 @@ const handleCreate = async () => {
190
190
  tenantId: tenantId.value!,
191
191
  }
192
192
 
193
- await createNamespace(requestBody)
194
- emit('success', { database: selectedDatabase.value, namespace: name.value })
195
- closeModal()
193
+ try {
194
+ await createNamespace(requestBody)
195
+ emit('success', { database: selectedDatabase.value, namespace: name.value })
196
+ closeModal()
197
+ }
198
+ catch {
199
+ error.value = t('knowledge.form.create_error')
200
+ }
201
+ finally {
202
+ isCreating.value = false
203
+ }
196
204
  }
197
205
 
198
206
  watch(() => props.modelValue, (isVisible) => {