@swiss-ai-hub/web 0.319.0 → 0.320.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/CronInput.vue +208 -0
- package/components/Knowledge/Document/List.vue +20 -9
- package/composables/app/useAppVersion.ts +30 -11
- package/composables/document/useScheduledDeletions.ts +16 -8
- package/formkit.config.ts +34 -3
- package/i18n/locales/de.yaml +24 -0
- package/i18n/locales/en.yaml +23 -0
- package/i18n/locales/fr.yaml +24 -0
- package/i18n/locales/it.yaml +24 -0
- package/middleware/auth.global.ts +34 -16
- package/package.json +1 -1
- package/pages/auth/login/[idp].vue +57 -0
- package/pages/auth/login/index.vue +46 -0
- 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 +18 -2
- package/sdk/client/schemas.gen.ts +614 -171
- package/sdk/client/sdk.gen.ts +72 -0
- package/sdk/client/types.gen.ts +568 -159
|
@@ -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
|
-
|
|
24
|
+
<slot name="heading" />
|
|
25
25
|
</h2>
|
|
26
26
|
<p class="text-surface-400">
|
|
27
|
-
|
|
27
|
+
<slot name="message" />
|
|
28
28
|
</p>
|
|
29
29
|
</div>
|
|
30
30
|
<div class="flex flex-col gap-3">
|
|
31
|
-
<
|
|
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>
|
|
@@ -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>
|
|
@@ -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
|
|
|
@@ -3,18 +3,29 @@ import { minutesToMilliseconds } from 'date-fns'
|
|
|
3
3
|
|
|
4
4
|
// Surfaces the running version of both services. The UI service version is baked
|
|
5
5
|
// into the static bundle at build time (runtimeConfig.public.appVersion); the API
|
|
6
|
-
// version is read from the health endpoint
|
|
7
|
-
// `latest`) the running image was pulled as. When both match a single number is
|
|
6
|
+
// version is read from the health endpoint. When both agree a single number is
|
|
8
7
|
// shown, otherwise both are shown as `UI / API`.
|
|
9
8
|
//
|
|
10
|
-
// A release is promoted
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
9
|
+
// A release is promoted by retagging the exact `-rc.N` build that was tested, so
|
|
10
|
+
// the baked UI version keeps its release-candidate suffix (e.g. `v0.317.0-rc.3`)
|
|
11
|
+
// even though the artifact IS the final release. A container cannot read its own
|
|
12
|
+
// image tag, so the release has to be inferred from what the API reports, and the
|
|
13
|
+
// two deployment flavours report it differently: docker-compose pins the channel
|
|
14
|
+
// tag (`latest`), while Helm pins an exact `vX.Y.Z` per instance and leaves the
|
|
15
|
+
// API on its package metadata (`0.319.0`). Both are release signals; on either
|
|
16
|
+
// one the suffix is dropped so a promoted build shows its release version.
|
|
17
|
+
//
|
|
18
|
+
// Versions are compared with the leading `v` normalised away, because one side is
|
|
19
|
+
// a git tag (`v0.319.0`) and the other Python package metadata (`0.319.0`) — the
|
|
20
|
+
// same version, and rendering it as `UI / API` would read as skew.
|
|
21
|
+
//
|
|
22
|
+
// Every other case is genuinely a candidate or a rolling build (another channel
|
|
23
|
+
// such as `staging`, an API pinned to an explicit `-rc.N`, a nightly), so the
|
|
24
|
+
// suffix stays to keep the build identifiable.
|
|
16
25
|
const RELEASE_CHANNEL = 'latest'
|
|
17
|
-
const
|
|
26
|
+
const RC_SUFFIX = /-rc\.\d+$/
|
|
27
|
+
const toReleaseVersion = (version: string): string => version.replace(RC_SUFFIX, '')
|
|
28
|
+
const toComparable = (version: string): string => version.replace(/^v/, '')
|
|
18
29
|
|
|
19
30
|
export const useAppVersion = defineQuery(() => {
|
|
20
31
|
const bakedUiVersion = useRuntimeConfig().public.appVersion as string
|
|
@@ -28,13 +39,21 @@ export const useAppVersion = defineQuery(() => {
|
|
|
28
39
|
},
|
|
29
40
|
})
|
|
30
41
|
|
|
42
|
+
// An API version that still carries `-rc.N` can never equal the stripped UI
|
|
43
|
+
// version, so a candidate deployment fails this check without a separate guard.
|
|
44
|
+
const isReleaseDeployment = computed(() => {
|
|
45
|
+
const api = apiVersion.value
|
|
46
|
+
if (!api) return false
|
|
47
|
+
return api === RELEASE_CHANNEL || toComparable(api) === toComparable(toReleaseVersion(bakedUiVersion))
|
|
48
|
+
})
|
|
49
|
+
|
|
31
50
|
const uiVersion = computed(() =>
|
|
32
|
-
|
|
51
|
+
isReleaseDeployment.value ? toReleaseVersion(bakedUiVersion) : bakedUiVersion,
|
|
33
52
|
)
|
|
34
53
|
|
|
35
54
|
const versionDisplay = computed(() => {
|
|
36
55
|
const api = apiVersion.value
|
|
37
|
-
if (!api || api === uiVersion.value) return uiVersion.value
|
|
56
|
+
if (!api || toComparable(api) === toComparable(uiVersion.value)) return uiVersion.value
|
|
38
57
|
return `${uiVersion.value} / ${api}`
|
|
39
58
|
})
|
|
40
59
|
|
|
@@ -2,11 +2,15 @@ import { useStorage } from '@vueuse/core'
|
|
|
2
2
|
|
|
3
3
|
import type { MaybeRefOrGetter } from 'vue'
|
|
4
4
|
|
|
5
|
-
// Deletion is eventual (the pipeline cleans the stores
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
5
|
+
// Deletion is eventual (the pipeline cleans the stores well after the API returns), so the list keeps
|
|
6
|
+
// returning a deleted document for a while. We persist scheduled ids in localStorage — surviving page
|
|
7
|
+
// refreshes — to drive a "Deleting" badge, and expire them after a TTL so a failed cleanup eventually
|
|
8
|
+
// re-surfaces the row instead of hiding it forever.
|
|
9
|
+
//
|
|
10
|
+
// The TTL has to cover a Dagster backlog, not just a slow run: the cleanup waits on a NATS sensor tick,
|
|
11
|
+
// then the observe job's turn in the queue, then a run-status sensor tick, then the remove job's turn.
|
|
12
|
+
// A busy instance can push that out for hours, so a day is the honest bound.
|
|
13
|
+
const TTL_MS = 24 * 60 * 60 * 1000
|
|
10
14
|
|
|
11
15
|
const store = useStorage<Record<string, number>>('aihub:scheduled-deletions', {})
|
|
12
16
|
|
|
@@ -15,9 +19,13 @@ function entryKey(database: string, namespace: string, documentId: string): stri
|
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export function useScheduledDeletions(database: MaybeRefOrGetter<string>, namespace: MaybeRefOrGetter<string>) {
|
|
22
|
+
function scheduledAt(documentId: string): number | undefined {
|
|
23
|
+
const at = store.value[entryKey(toValue(database), toValue(namespace), documentId)]
|
|
24
|
+
return at != null && Date.now() - at < TTL_MS ? at : undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
function isScheduled(documentId: string): boolean {
|
|
19
|
-
|
|
20
|
-
return scheduledAt != null && Date.now() - scheduledAt < TTL_MS
|
|
28
|
+
return scheduledAt(documentId) != null
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
function schedule(documentIds: string[]): void {
|
|
@@ -44,5 +52,5 @@ export function useScheduledDeletions(database: MaybeRefOrGetter<string>, namesp
|
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
return { isScheduled, schedule, unschedule }
|
|
55
|
+
return { isScheduled, scheduledAt, schedule, unschedule }
|
|
48
56
|
}
|
package/formkit.config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AgentSelector from '@core/components/FormKit/AgentSelector.vue'
|
|
2
2
|
import ChipsInput from '@core/components/FormKit/ChipsInput.vue'
|
|
3
|
+
import CronInput from '@core/components/FormKit/CronInput.vue'
|
|
3
4
|
import IconSelector from '@core/components/FormKit/IconSelector.vue'
|
|
4
5
|
import KnowledgeDatabaseSelector from '@core/components/FormKit/KnowledgeDatabaseSelector.vue'
|
|
5
6
|
import LocaleInput from '@core/components/FormKit/LocaleInput.vue'
|
|
@@ -11,7 +12,7 @@ import { createInput } from '@formkit/vue'
|
|
|
11
12
|
import { primeInputs } from '@sfxcode/formkit-primevue'
|
|
12
13
|
|
|
13
14
|
import type { FormKitNode } from '@formkit/core'
|
|
14
|
-
import type { DefaultConfigOptions } from '@formkit/vue'
|
|
15
|
+
import type { DefaultConfigOptions, PluginConfigs } from '@formkit/vue'
|
|
15
16
|
|
|
16
17
|
const LOCALES = ['de', 'en', 'fr', 'it'] as const
|
|
17
18
|
|
|
@@ -31,6 +32,21 @@ function localeRequired(node: FormKitNode): boolean {
|
|
|
31
32
|
// create form: precisely the case this rule exists to catch.
|
|
32
33
|
localeRequired.skipEmpty = false
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Passes when the value is listed in the sibling field named by `address`, or when that list is
|
|
37
|
+
* empty — backend allow-lists treat empty as unrestricted. Reading the sibling through `node.at()`
|
|
38
|
+
* registers it as a validation dependency, so FormKit re-runs this rule when the list itself
|
|
39
|
+
* changes, not only when this field does.
|
|
40
|
+
*
|
|
41
|
+
* Advisory only: it exists so an admin sees the conflict in the form. The backend never depends on
|
|
42
|
+
* it — a value outside the allow-list is rejected where it is actually used.
|
|
43
|
+
*/
|
|
44
|
+
const memberOf: PluginConfigs['rules'][string] = (node, address: string) => {
|
|
45
|
+
const allowed = node.at(address)?.value
|
|
46
|
+
if (!Array.isArray(allowed) || allowed.length === 0) return true
|
|
47
|
+
return allowed.includes(node.value)
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
const localeRequiredMessages = {
|
|
35
51
|
de: 'Mindestens eine Sprache muss ausgefüllt sein.',
|
|
36
52
|
en: 'At least one language must be filled in.',
|
|
@@ -38,10 +54,22 @@ const localeRequiredMessages = {
|
|
|
38
54
|
it: 'Almeno una lingua deve essere compilata.',
|
|
39
55
|
}
|
|
40
56
|
|
|
57
|
+
const memberOfMessages = {
|
|
58
|
+
de: 'Dieser Wert steht nicht in der Liste der erlaubten Werte.',
|
|
59
|
+
en: 'This value is not in the allowed list.',
|
|
60
|
+
fr: 'Cette valeur ne figure pas dans la liste des valeurs autorisées.',
|
|
61
|
+
it: 'Questo valore non è presente nell\'elenco dei valori consentiti.',
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
const config: DefaultConfigOptions = {
|
|
42
|
-
rules: { localeRequired },
|
|
65
|
+
rules: { localeRequired, memberOf },
|
|
43
66
|
messages: Object.fromEntries(
|
|
44
|
-
LOCALES.map(locale => [locale, {
|
|
67
|
+
LOCALES.map(locale => [locale, {
|
|
68
|
+
validation: {
|
|
69
|
+
localeRequired: localeRequiredMessages[locale],
|
|
70
|
+
memberOf: memberOfMessages[locale],
|
|
71
|
+
},
|
|
72
|
+
}]),
|
|
45
73
|
),
|
|
46
74
|
inputs: {
|
|
47
75
|
...primeInputs,
|
|
@@ -51,6 +79,9 @@ const config: DefaultConfigOptions = {
|
|
|
51
79
|
chipsInput: createInput(ChipsInput, {
|
|
52
80
|
props: ['placeholder'],
|
|
53
81
|
}),
|
|
82
|
+
cronInput: createInput(CronInput, {
|
|
83
|
+
props: ['timezonePlaceholder', 'filter'],
|
|
84
|
+
}),
|
|
54
85
|
knowledgeDatabaseSelector: createInput(KnowledgeDatabaseSelector, {
|
|
55
86
|
props: ['placeholder', 'filter'],
|
|
56
87
|
}),
|
package/i18n/locales/de.yaml
CHANGED
|
@@ -557,7 +557,9 @@ auth:
|
|
|
557
557
|
description: Bitte melden Sie sich an, um auf die Anwendung zuzugreifen
|
|
558
558
|
tagline: Erleben Sie die Zukunft der künstlichen Intelligenz
|
|
559
559
|
welcome: Willkommen im AI Hub von {companyName}
|
|
560
|
+
welcomeProvider: Willkommen im AI Hub — Anmeldung mit {provider}
|
|
560
561
|
pleaseLogin: Bitte melden Sie sich an
|
|
562
|
+
pleaseLoginWith: Bitte melden Sie sich mit {provider} an
|
|
561
563
|
loginWith: Anmelden mit {provider}
|
|
562
564
|
customSolutions: Massgeschneiderte KI-Lösungen für Ihr Unternehmen
|
|
563
565
|
logoAlt: Logo des Unternehmens
|
|
@@ -795,6 +797,28 @@ form:
|
|
|
795
797
|
nicht wie erwartet. Um unerwartetes Verhalten zu vermeiden, deaktivieren Sie
|
|
796
798
|
sie bitte vorübergehend, bis eine Korrektur veröffentlicht wird.
|
|
797
799
|
lib:
|
|
800
|
+
cron:
|
|
801
|
+
errors:
|
|
802
|
+
required: Jede Position ist erforderlich — eine leere ergibt keinen gültigen
|
|
803
|
+
Zeitplan.
|
|
804
|
+
invalid: Hier sind nur Ziffern und die Zeichen * , - / ? erlaubt.
|
|
805
|
+
range: Werte müssen zwischen {min} und {max} liegen.
|
|
806
|
+
expression: 'Resultierender Cron-Ausdruck:'
|
|
807
|
+
minute:
|
|
808
|
+
label: Minute
|
|
809
|
+
hour:
|
|
810
|
+
label: Stunde
|
|
811
|
+
day_of_month:
|
|
812
|
+
label: Tag des Monats
|
|
813
|
+
month:
|
|
814
|
+
label: Monat
|
|
815
|
+
day_of_week:
|
|
816
|
+
label: Wochentag
|
|
817
|
+
timezone:
|
|
818
|
+
label: Zeitzone
|
|
819
|
+
placeholder: Zeitzone auswählen
|
|
820
|
+
help: Die Cron-Positionen werden in dieser Zeitzone interpretiert, damit ein
|
|
821
|
+
täglicher Zeitplan über Zeitumstellungen hinweg dieselbe Uhrzeit behält.
|
|
798
822
|
vectorStore:
|
|
799
823
|
label: Vektorspeicher
|
|
800
824
|
help: Wissensdatenbank und Namensräume für die Suche auswählen
|
package/i18n/locales/en.yaml
CHANGED
|
@@ -548,7 +548,9 @@ auth:
|
|
|
548
548
|
description: Please log in to access the application
|
|
549
549
|
tagline: Experience the future of artificial intelligence
|
|
550
550
|
welcome: Welcome to the AI Hub of {companyName}
|
|
551
|
+
welcomeProvider: Welcome to the AI Hub — sign in with {provider}
|
|
551
552
|
pleaseLogin: Please log in
|
|
553
|
+
pleaseLoginWith: Please log in with {provider}
|
|
552
554
|
loginWith: Login with {provider}
|
|
553
555
|
customSolutions: Custom AI solutions for your company
|
|
554
556
|
logoAlt: Logo of company
|
|
@@ -782,6 +784,27 @@ form:
|
|
|
782
784
|
org_memory_performance: This feature may not work as expected in some cases. To
|
|
783
785
|
avoid unexpected behavior, please temporarily disable it until a fix is released.
|
|
784
786
|
lib:
|
|
787
|
+
cron:
|
|
788
|
+
errors:
|
|
789
|
+
required: Every position is required — a blank one is not a valid schedule.
|
|
790
|
+
invalid: Only digits and the characters * , - / ? are allowed here.
|
|
791
|
+
range: Values must be between {min} and {max}.
|
|
792
|
+
expression: 'Resulting cron expression:'
|
|
793
|
+
minute:
|
|
794
|
+
label: Minute
|
|
795
|
+
hour:
|
|
796
|
+
label: Hour
|
|
797
|
+
day_of_month:
|
|
798
|
+
label: Day of month
|
|
799
|
+
month:
|
|
800
|
+
label: Month
|
|
801
|
+
day_of_week:
|
|
802
|
+
label: Day of week
|
|
803
|
+
timezone:
|
|
804
|
+
label: Timezone
|
|
805
|
+
placeholder: Select a timezone
|
|
806
|
+
help: The cron positions are interpreted in this timezone, so a daily schedule
|
|
807
|
+
keeps the same wall-clock time across daylight-saving changes.
|
|
785
808
|
vectorStore:
|
|
786
809
|
label: Vector Store
|
|
787
810
|
help: Select the knowledge database and namespaces to search
|
package/i18n/locales/fr.yaml
CHANGED
|
@@ -556,7 +556,9 @@ auth:
|
|
|
556
556
|
description: Veuillez vous connecter pour accéder à l'application
|
|
557
557
|
tagline: Découvrez l'avenir de l'intelligence artificielle
|
|
558
558
|
welcome: Bienvenue sur le AI Hub de {companyName}
|
|
559
|
+
welcomeProvider: Bienvenue sur le AI Hub — connexion avec {provider}
|
|
559
560
|
pleaseLogin: Veuillez vous connecter
|
|
561
|
+
pleaseLoginWith: Veuillez vous connecter avec {provider}
|
|
560
562
|
loginWith: Se connecter avec {provider}
|
|
561
563
|
customSolutions: Solutions d'IA personnalisées pour votre entreprise
|
|
562
564
|
logoAlt: Logo de l'entreprise
|
|
@@ -795,6 +797,28 @@ form:
|
|
|
795
797
|
dans certains cas. Pour éviter tout comportement inattendu, veuillez la désactiver
|
|
796
798
|
temporairement jusqu'à la publication d'un correctif.
|
|
797
799
|
lib:
|
|
800
|
+
cron:
|
|
801
|
+
errors:
|
|
802
|
+
required: Chaque position est requise — une position vide ne constitue pas une
|
|
803
|
+
planification valide.
|
|
804
|
+
invalid: Seuls les chiffres et les caractères * , - / ? sont autorisés ici.
|
|
805
|
+
range: Les valeurs doivent être comprises entre {min} et {max}.
|
|
806
|
+
expression: 'Expression cron résultante :'
|
|
807
|
+
minute:
|
|
808
|
+
label: Minute
|
|
809
|
+
hour:
|
|
810
|
+
label: Heure
|
|
811
|
+
day_of_month:
|
|
812
|
+
label: Jour du mois
|
|
813
|
+
month:
|
|
814
|
+
label: Mois
|
|
815
|
+
day_of_week:
|
|
816
|
+
label: Jour de la semaine
|
|
817
|
+
timezone:
|
|
818
|
+
label: Fuseau horaire
|
|
819
|
+
placeholder: Sélectionner un fuseau horaire
|
|
820
|
+
help: Les positions cron sont interprétées dans ce fuseau horaire, afin qu'une
|
|
821
|
+
planification quotidienne conserve la même heure lors des changements d'heure.
|
|
798
822
|
vectorStore:
|
|
799
823
|
label: Stockage vectoriel
|
|
800
824
|
help: Sélectionner la base de connaissances et les espaces de noms à rechercher
|
package/i18n/locales/it.yaml
CHANGED
|
@@ -554,7 +554,9 @@ auth:
|
|
|
554
554
|
description: Effettua il login per accedere all'applicazione
|
|
555
555
|
tagline: Sperimenta il futuro dell'intelligenza artificiale
|
|
556
556
|
welcome: Benvenuti nell'AI Hub di {companyName}
|
|
557
|
+
welcomeProvider: Benvenuti nell'AI Hub — accesso con {provider}
|
|
557
558
|
pleaseLogin: Effettua il login
|
|
559
|
+
pleaseLoginWith: Effettua il login con {provider}
|
|
558
560
|
loginWith: Accedi con {provider}
|
|
559
561
|
customSolutions: Soluzioni AI personalizzate per la tua azienda
|
|
560
562
|
logoAlt: Logo dell'azienda
|
|
@@ -791,6 +793,28 @@ form:
|
|
|
791
793
|
in alcuni casi. Per evitare comportamenti inattesi, disattivala temporaneamente
|
|
792
794
|
fino al rilascio di una correzione.
|
|
793
795
|
lib:
|
|
796
|
+
cron:
|
|
797
|
+
errors:
|
|
798
|
+
required: Ogni posizione è obbligatoria — una vuota non è una pianificazione
|
|
799
|
+
valida.
|
|
800
|
+
invalid: Qui sono ammessi solo cifre e i caratteri * , - / ? .
|
|
801
|
+
range: I valori devono essere compresi tra {min} e {max}.
|
|
802
|
+
expression: 'Espressione cron risultante:'
|
|
803
|
+
minute:
|
|
804
|
+
label: Minuto
|
|
805
|
+
hour:
|
|
806
|
+
label: Ora
|
|
807
|
+
day_of_month:
|
|
808
|
+
label: Giorno del mese
|
|
809
|
+
month:
|
|
810
|
+
label: Mese
|
|
811
|
+
day_of_week:
|
|
812
|
+
label: Giorno della settimana
|
|
813
|
+
timezone:
|
|
814
|
+
label: Fuso orario
|
|
815
|
+
placeholder: Seleziona un fuso orario
|
|
816
|
+
help: Le posizioni cron sono interpretate in questo fuso orario, così una pianificazione
|
|
817
|
+
giornaliera mantiene la stessa ora durante i cambi di orario.
|
|
794
818
|
vectorStore:
|
|
795
819
|
label: Archivio vettoriale
|
|
796
820
|
help: Seleziona il database di conoscenza e gli spazi dei nomi da cercare
|