@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.
- package/{pages/auth/login.vue → components/Auth/LoginPanel.vue} +3 -33
- package/components/FormKit/AgentSelector.vue +20 -7
- package/components/FormKit/CronInput.vue +208 -0
- package/components/FormKit/Repeater.vue +1 -4
- package/components/FormKit/VectorStoreInput.vue +43 -6
- package/components/Knowledge/Database/CreateModal.vue +315 -0
- package/components/Knowledge/Database/EmptyCard.vue +34 -0
- package/components/Knowledge/DeleteConfirmModal.vue +87 -0
- package/components/Knowledge/Document/List.vue +20 -9
- package/components/Knowledge/Namespace/Card.vue +18 -0
- package/components/Knowledge/Namespace/CreateModal.vue +11 -3
- package/components/Role/AccessCapabilities.vue +4 -4
- package/components/Role/AccessCapabilityGroup.vue +22 -6
- package/components/Role/AccessRulesEditor.vue +15 -12
- package/composables/app/useAppVersion.ts +37 -20
- package/composables/auth/useAuth.ts +3 -11
- package/composables/document/useCreateDatabase.ts +21 -0
- package/composables/document/useDeleteDatabase.ts +29 -0
- package/composables/document/useDeleteDocument.ts +1 -1
- package/composables/document/useDeleteDocuments.ts +1 -1
- package/composables/document/useDeleteNamespace.ts +30 -0
- package/composables/document/useIngestors.ts +23 -0
- package/composables/document/useScheduledDeletions.ts +16 -8
- package/composables/form/useCreateInstanceForm.ts +2 -2
- package/composables/form/useFormKitTransform.ts +44 -15
- package/formkit.config.ts +57 -3
- package/i18n/locales/de.yaml +84 -5
- package/i18n/locales/en.yaml +80 -3
- package/i18n/locales/fr.yaml +82 -3
- package/i18n/locales/it.yaml +84 -3
- package/middleware/agent-admin.ts +31 -0
- package/middleware/auth.global.ts +34 -16
- package/package.json +1 -1
- package/pages/[tenant]/service/agents.vue +3 -0
- package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id].vue +12 -5
- package/pages/[tenant]/service/knowledge.vue +138 -0
- package/pages/auth/login/[idp].vue +57 -0
- package/pages/auth/login/index.vue +46 -0
- package/plugins/0.runtime-config.client.ts +5 -0
- package/plugins/oidc-client.ts +1 -1
- package/sdk/client/client/client.gen.ts +1 -3
- package/sdk/client/client/types.gen.ts +7 -13
- package/sdk/client/client/utils.gen.ts +1 -2
- package/sdk/client/core/queryKeySerializer.gen.ts +1 -6
- package/sdk/client/core/types.gen.ts +5 -3
- package/sdk/client/index.ts +47 -4
- package/sdk/client/schemas.gen.ts +1787 -780
- package/sdk/client/sdk.gen.ts +231 -1
- package/sdk/client/types.gen.ts +1103 -226
- package/plugins/keycloak-client.ts +0 -41
|
@@ -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>
|
|
@@ -140,7 +140,7 @@ const { tenantId } = useTenant()
|
|
|
140
140
|
const { getDocumentSourceUrl } = useDocumentUrl()
|
|
141
141
|
const { deleteDocument, isDeleting } = useDeleteDocument()
|
|
142
142
|
const { deleteDocuments, isDeleting: isBatchDeleting } = useDeleteDocuments()
|
|
143
|
-
const { isScheduled, schedule, unschedule } = useScheduledDeletions(
|
|
143
|
+
const { isScheduled, scheduledAt, schedule, unschedule } = useScheduledDeletions(
|
|
144
144
|
() => route.params.db as string,
|
|
145
145
|
() => route.params.namespace as string,
|
|
146
146
|
)
|
|
@@ -163,18 +163,24 @@ const checkedDocuments = ref<DocumentDto[]>([])
|
|
|
163
163
|
|
|
164
164
|
const formatted = (datestr: string) => useDateFormat(new Date(datestr), 'DD.MM.YYYY')
|
|
165
165
|
|
|
166
|
-
const isDocumentDeleting = (document: DocumentDto) => isScheduled(document.id)
|
|
166
|
+
const isDocumentDeleting = (document: DocumentDto) => isScheduled(document.id)
|
|
167
167
|
|
|
168
|
-
//
|
|
169
|
-
|
|
168
|
+
// Deleting is safe at any stage of ingestion: the API only removes the source file, and the pipeline
|
|
169
|
+
// drops the document's partition before the remove job touches any store, which aborts an in-flight
|
|
170
|
+
// run before it writes. So the only reason to withhold the action is a deletion already in flight.
|
|
171
|
+
const isDocumentDeletable = (document: DocumentDto) => !isDocumentDeleting(document)
|
|
170
172
|
|
|
171
|
-
// Ids derive from the source URI, so re-uploading a just-deleted file reuses its id
|
|
172
|
-
//
|
|
173
|
+
// Ids derive from the source URI, so re-uploading a just-deleted file reuses its id and would inherit
|
|
174
|
+
// its "Deleting" badge. An upload rewrites the document, so an updated_at newer than the moment we
|
|
175
|
+
// scheduled the deletion means this row is the replacement, not the one still on its way out.
|
|
173
176
|
watch(
|
|
174
177
|
() => props.documents,
|
|
175
178
|
(documents) => {
|
|
176
179
|
const reUploadedIds = (documents ?? [])
|
|
177
|
-
.filter(document =>
|
|
180
|
+
.filter((document) => {
|
|
181
|
+
const requestedAt = scheduledAt(document.id)
|
|
182
|
+
return requestedAt != null && new Date(document.updated_at).getTime() > requestedAt
|
|
183
|
+
})
|
|
178
184
|
.map(document => document.id)
|
|
179
185
|
if (reUploadedIds.length > 0) {
|
|
180
186
|
unschedule(reUploadedIds)
|
|
@@ -200,15 +206,20 @@ const onSelectAllChange = ({ checked }: { checked: boolean }) => {
|
|
|
200
206
|
|
|
201
207
|
const handleRowClick = (event: DataTableRowClickEvent) => {
|
|
202
208
|
const document = event.data as DocumentDto
|
|
203
|
-
if (
|
|
209
|
+
if (!isDocumentDeleting(document)) {
|
|
204
210
|
emit('selected', document)
|
|
205
211
|
}
|
|
206
212
|
}
|
|
207
213
|
|
|
214
|
+
// Only a deletion in flight makes a row inert. A document still being ingested stays dimmed but usable —
|
|
215
|
+
// otherwise a failed ingestion leaves a row that can be neither opened nor removed.
|
|
208
216
|
const getRowClass = (data: DocumentDto) => {
|
|
209
|
-
if (
|
|
217
|
+
if (isDocumentDeleting(data)) {
|
|
210
218
|
return 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
211
219
|
}
|
|
220
|
+
if (!data.is_ingested) {
|
|
221
|
+
return 'opacity-50'
|
|
222
|
+
}
|
|
212
223
|
return data.id === route.params.document_id ? 'bg-surface-100 dark:bg-surface-800' : ''
|
|
213
224
|
}
|
|
214
225
|
|
|
@@ -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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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) => {
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
:group="group"
|
|
22
22
|
:depth="0"
|
|
23
23
|
:readonly="readonly"
|
|
24
|
-
@add="(
|
|
25
|
-
@remove="(
|
|
24
|
+
@add="(rules) => emit('add', rules)"
|
|
25
|
+
@remove="(rules) => emit('remove', rules)"
|
|
26
26
|
/>
|
|
27
27
|
</div>
|
|
28
28
|
<p
|
|
@@ -55,8 +55,8 @@ const props = withDefaults(defineProps<{
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
const emit = defineEmits<{
|
|
58
|
-
add: [
|
|
59
|
-
remove: [
|
|
58
|
+
add: [rules: string[]]
|
|
59
|
+
remove: [rules: string[]]
|
|
60
60
|
}>()
|
|
61
61
|
|
|
62
62
|
const { capabilities, capabilitiesAreLoading } = useAccessCapabilities(
|
|
@@ -78,8 +78,9 @@
|
|
|
78
78
|
</span>
|
|
79
79
|
<code
|
|
80
80
|
v-if="cap.rule"
|
|
81
|
+
v-tooltip.top="ruleTooltip(cap)"
|
|
81
82
|
class="mt-0.5 shrink-0 font-mono text-[11px] text-surface-300 transition-colors group-hover/cap:text-surface-500 dark:text-surface-600 dark:group-hover/cap:text-surface-400"
|
|
82
|
-
>{{ cap.rule }}</code>
|
|
83
|
+
>{{ cap.companion_rules?.length ? `${cap.rule} +${cap.companion_rules.length}` : cap.rule }}</code>
|
|
83
84
|
</label>
|
|
84
85
|
</div>
|
|
85
86
|
|
|
@@ -95,8 +96,8 @@
|
|
|
95
96
|
:group="sub"
|
|
96
97
|
:depth="depth + 1"
|
|
97
98
|
:readonly="readonly"
|
|
98
|
-
@add="(
|
|
99
|
-
@remove="(
|
|
99
|
+
@add="(rules) => emit('add', rules)"
|
|
100
|
+
@remove="(rules) => emit('remove', rules)"
|
|
100
101
|
/>
|
|
101
102
|
</div>
|
|
102
103
|
</div>
|
|
@@ -119,12 +120,27 @@ const props = withDefaults(defineProps<{
|
|
|
119
120
|
})
|
|
120
121
|
|
|
121
122
|
const emit = defineEmits<{
|
|
122
|
-
add: [
|
|
123
|
-
remove: [
|
|
123
|
+
add: [rules: string[]]
|
|
124
|
+
remove: [rules: string[]]
|
|
124
125
|
}>()
|
|
125
126
|
|
|
127
|
+
// Two lists, two meanings. `companion_rules` is written and cleared with the row — a knowledge database
|
|
128
|
+
// owns its namespaces, and the grammar has no single form covering a node and its subtree, so that row
|
|
129
|
+
// needs both. `revoked_rules` is only ever cleared: an agent class does not own the profiles built from it,
|
|
130
|
+
// so ticking must never write `<class>.>`, while unticking still takes an old ceiling's copy of it. Each
|
|
131
|
+
// direction emits one payload because the parent writes them through a single `v-model`, which cannot
|
|
132
|
+
// absorb two writes in the same tick — the second would read a model value the first had not yet updated.
|
|
133
|
+
// The badge counts only what ticking grants; what unticking additionally takes is left to the tooltip.
|
|
134
|
+
const ruleTooltip = (cap: Capability) =>
|
|
135
|
+
[
|
|
136
|
+
cap.companion_rules?.length ? t('role.capability_also_granted', { rules: cap.companion_rules.join(', ') }) : null,
|
|
137
|
+
cap.revoked_rules?.length ? t('role.capability_also_cleared', { rules: cap.revoked_rules.join(', ') }) : null,
|
|
138
|
+
].filter(Boolean).join('\n') || undefined
|
|
139
|
+
|
|
126
140
|
const onToggle = (cap: Capability, value: boolean) => {
|
|
127
141
|
if (props.readonly || !cap.rule) return
|
|
128
|
-
|
|
142
|
+
const granted = [cap.rule, ...(cap.companion_rules ?? [])]
|
|
143
|
+
if (value) emit('add', granted)
|
|
144
|
+
else emit('remove', [...granted, ...(cap.revoked_rules ?? [])])
|
|
129
145
|
}
|
|
130
146
|
</script>
|