@cat-factory/app 0.149.0 → 0.150.1
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/app/components/providers/ApiKeysSection.vue +47 -7
- package/app/components/providers/VendorCredentialsModal.vue +56 -7
- package/app/components/settings/KubernetesEnvironmentForm.vue +45 -29
- package/app/composables/api/models.ts +22 -0
- package/app/composables/usePipelineErrorToast.ts +206 -175
- package/app/stores/apiKeys.ts +30 -1
- package/app/stores/vendorCredentials.ts +32 -2
- package/app/types/models.ts +2 -0
- package/i18n/locales/de.json +14 -4
- package/i18n/locales/en.json +14 -4
- package/i18n/locales/es.json +14 -4
- package/i18n/locales/fr.json +14 -4
- package/i18n/locales/he.json +14 -4
- package/i18n/locales/it.json +14 -4
- package/i18n/locales/ja.json +14 -4
- package/i18n/locales/pl.json +14 -4
- package/i18n/locales/tr.json +14 -4
- package/i18n/locales/uk.json +14 -4
- package/package.json +2 -2
|
@@ -186,200 +186,231 @@ export function parseConflict(
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/** The non-null parsed shape of a backend conflict, as returned by {@link parseConflict}. */
|
|
190
|
+
type ParsedConflict = NonNullable<ReturnType<typeof parseConflict>>
|
|
191
|
+
|
|
189
192
|
export function usePipelineErrorToast() {
|
|
190
193
|
const toast = useToast()
|
|
191
194
|
const ui = useUiStore()
|
|
192
195
|
const { t, te } = useI18n()
|
|
193
196
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
197
|
+
// The headline case: a pipeline step's model has no usable provider. Name the
|
|
198
|
+
// offending model(s), explain no provider is available, and offer the one-click jump
|
|
199
|
+
// to the AI setup — the same remedy the startup "No AI model configured" banner gives.
|
|
200
|
+
function presentProvidersUnconfigured(conflict: ParsedConflict): void {
|
|
201
|
+
const models = Array.isArray(conflict.details.models) ? conflict.details.models : []
|
|
202
|
+
const list = models.join(', ')
|
|
203
|
+
toast.add({
|
|
204
|
+
title: t('errors.conflict.providersUnconfigured.title'),
|
|
205
|
+
description: list
|
|
206
|
+
? t('errors.conflict.providersUnconfigured.body', { models: list })
|
|
207
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
208
|
+
color: 'error',
|
|
209
|
+
icon: 'i-lucide-cpu',
|
|
210
|
+
// Stay until dismissed: an actionable toast whose remedy button vanishes on the ~5s
|
|
211
|
+
// auto-dismiss takes the one-click fix with it before the user can reach it.
|
|
212
|
+
duration: 0,
|
|
213
|
+
actions: [
|
|
214
|
+
{
|
|
215
|
+
label: t('errors.conflict.providersUnconfigured.action'),
|
|
216
|
+
icon: 'i-lucide-settings',
|
|
217
|
+
onClick: () => ui.openAiProviderSetup(),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
})
|
|
221
|
+
}
|
|
200
222
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
223
|
+
// A pipeline step relies on binary-artifact storage (the UI Tester uploads screenshots)
|
|
224
|
+
// but the account has none configured. Explain it and offer the jump to the content-storage
|
|
225
|
+
// settings — the same shape as the providers-unconfigured case above. Prefer the localized
|
|
226
|
+
// body (it carries no runtime interpolation) so non-English users see translated copy; the
|
|
227
|
+
// raw backend prose is only the last-resort fallback when the locale lacks the key.
|
|
228
|
+
function presentBinaryStorageUnconfigured(conflict: ParsedConflict): void {
|
|
229
|
+
toast.add({
|
|
230
|
+
title: t('errors.conflict.binaryStorageUnconfigured.title'),
|
|
231
|
+
description: te('errors.conflict.binaryStorageUnconfigured.body')
|
|
232
|
+
? t('errors.conflict.binaryStorageUnconfigured.body')
|
|
233
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
234
|
+
color: 'error',
|
|
235
|
+
icon: 'i-lucide-image',
|
|
236
|
+
// Sticky, like the providers-unconfigured toast above: keep the "Configure storage"
|
|
237
|
+
// remedy reachable instead of letting it auto-dismiss.
|
|
238
|
+
duration: 0,
|
|
239
|
+
actions: [
|
|
240
|
+
{
|
|
241
|
+
label: t('errors.conflict.binaryStorageUnconfigured.action'),
|
|
242
|
+
icon: 'i-lucide-settings',
|
|
243
|
+
onClick: () => ui.openContentStorageSettings(),
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
})
|
|
247
|
+
}
|
|
227
248
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
// A pipeline includes a Deployer, but the SERVICE's ephemeral-environment config (the in-repo
|
|
250
|
+
// "what/where") is incomplete for its declared type. Steer the user straight to THAT service's
|
|
251
|
+
// environment config — the compose wizard for docker-compose, the service inspector otherwise —
|
|
252
|
+
// falling back to the workspace infrastructure window if the frame id wasn't carried.
|
|
253
|
+
function presentDeployerServiceConfig(conflict: ParsedConflict): void {
|
|
254
|
+
const frameId =
|
|
255
|
+
typeof conflict.details.frameId === 'string' ? conflict.details.frameId : undefined
|
|
256
|
+
const provisionType =
|
|
257
|
+
typeof conflict.details.provisionType === 'string'
|
|
258
|
+
? conflict.details.provisionType
|
|
259
|
+
: undefined
|
|
260
|
+
const missing = Array.isArray(conflict.details.missing)
|
|
261
|
+
? conflict.details.missing.join(', ')
|
|
262
|
+
: ''
|
|
263
|
+
toast.add({
|
|
264
|
+
title: t('errors.conflict.deployerServiceConfig.title'),
|
|
265
|
+
description: missing
|
|
266
|
+
? t('errors.conflict.deployerServiceConfig.body', { missing })
|
|
267
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
268
|
+
color: 'error',
|
|
269
|
+
icon: 'i-lucide-server',
|
|
270
|
+
// Sticky, like the other actionable conflicts: keep the "Fix configuration" jump reachable.
|
|
271
|
+
duration: 0,
|
|
272
|
+
actions: [
|
|
273
|
+
{
|
|
274
|
+
label: t('errors.conflict.deployerServiceConfig.action'),
|
|
275
|
+
icon: 'i-lucide-settings',
|
|
276
|
+
onClick: () => {
|
|
277
|
+
if (frameId && provisionType === 'docker-compose') ui.openEnvironmentSetup(frameId)
|
|
278
|
+
else if (frameId) ui.select(frameId)
|
|
279
|
+
else ui.openProviderConnection('environment')
|
|
249
280
|
},
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
})
|
|
284
|
+
}
|
|
254
285
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
: (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
actions: [
|
|
279
|
-
{
|
|
280
|
-
label: t('errors.conflict.deployerServiceConfig.action'),
|
|
281
|
-
icon: 'i-lucide-settings',
|
|
282
|
-
onClick: () => {
|
|
283
|
-
if (frameId && provisionType === 'docker-compose') ui.openEnvironmentSetup(frameId)
|
|
284
|
-
else if (frameId) ui.select(frameId)
|
|
285
|
-
else ui.openProviderConnection('environment')
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
})
|
|
290
|
-
return
|
|
291
|
-
}
|
|
286
|
+
// A pipeline includes a Deployer and the service config is sound, but no WORKSPACE handler
|
|
287
|
+
// resolves for the service's provision type (missing or ambiguous). Steer to the Infrastructure
|
|
288
|
+
// window's Test-environments tab. (Also raised by the Tester start gate — same fix applies.)
|
|
289
|
+
function presentProvisionTypeUnhandled(conflict: ParsedConflict): void {
|
|
290
|
+
const type =
|
|
291
|
+
typeof conflict.details.provisionType === 'string' ? conflict.details.provisionType : ''
|
|
292
|
+
toast.add({
|
|
293
|
+
title: t('errors.conflict.provisionTypeUnhandled.title'),
|
|
294
|
+
description: type
|
|
295
|
+
? t('errors.conflict.provisionTypeUnhandled.body', { type })
|
|
296
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
297
|
+
color: 'error',
|
|
298
|
+
icon: 'i-lucide-server-cog',
|
|
299
|
+
duration: 0,
|
|
300
|
+
actions: [
|
|
301
|
+
{
|
|
302
|
+
label: t('errors.conflict.provisionTypeUnhandled.action'),
|
|
303
|
+
icon: 'i-lucide-settings',
|
|
304
|
+
onClick: () => ui.openProviderConnection('environment'),
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
})
|
|
308
|
+
}
|
|
292
309
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
// A pipeline includes a Deployer, the config is structurally complete, but the live connection
|
|
311
|
+
// probe of the resolved deployment integration failed (unreachable endpoint / apiserver, bad
|
|
312
|
+
// token). Surface the provider's failure detail and steer to the handler to fix + re-test it.
|
|
313
|
+
function presentDeployerConnectionFailed(conflict: ParsedConflict): void {
|
|
314
|
+
const detail = typeof conflict.details.detail === 'string' ? conflict.details.detail : undefined
|
|
315
|
+
toast.add({
|
|
316
|
+
title: t('errors.conflict.deployerConnectionFailed.title'),
|
|
317
|
+
description: detail
|
|
318
|
+
? t('errors.conflict.deployerConnectionFailed.body', { detail })
|
|
319
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
320
|
+
color: 'error',
|
|
321
|
+
icon: 'i-lucide-plug',
|
|
322
|
+
duration: 0,
|
|
323
|
+
actions: [
|
|
324
|
+
{
|
|
325
|
+
label: t('errors.conflict.deployerConnectionFailed.action'),
|
|
326
|
+
icon: 'i-lucide-settings',
|
|
327
|
+
onClick: () => ui.openProviderConnection('environment'),
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Dispatch the bespoke conflict reasons (a runtime-interpolated body + a "configure X" action,
|
|
335
|
+
* each with its own key namespace — the ones excluded from `CONFLICT_INFO`). Returns `true` when
|
|
336
|
+
* the reason was one of them (and the toast was raised), `false` to fall through to the generic
|
|
337
|
+
* map. The reason values are mutually exclusive, so dispatch order is irrelevant.
|
|
338
|
+
*/
|
|
339
|
+
function presentBespokeConflict(conflict: ParsedConflict): boolean {
|
|
340
|
+
switch (conflict.reason) {
|
|
341
|
+
case 'providers_unconfigured':
|
|
342
|
+
presentProvidersUnconfigured(conflict)
|
|
343
|
+
return true
|
|
344
|
+
case 'binary_storage_unconfigured':
|
|
345
|
+
presentBinaryStorageUnconfigured(conflict)
|
|
346
|
+
return true
|
|
347
|
+
case 'deployer_service_provisioning_incomplete':
|
|
348
|
+
presentDeployerServiceConfig(conflict)
|
|
349
|
+
return true
|
|
350
|
+
case 'provision_type_unhandled':
|
|
351
|
+
presentProvisionTypeUnhandled(conflict)
|
|
352
|
+
return true
|
|
353
|
+
case 'deployer_connection_test_failed':
|
|
354
|
+
presentDeployerConnectionFailed(conflict)
|
|
355
|
+
return true
|
|
356
|
+
default:
|
|
357
|
+
return false
|
|
316
358
|
}
|
|
359
|
+
}
|
|
317
360
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Per-reason copy from the exhaustive map: a translated title + description, and a jump
|
|
363
|
+
* action for the reasons a UI panel can fix. `te` (translation-exists) guards every lookup,
|
|
364
|
+
* so a key missing from the active locale falls back rather than leaking a raw key: the
|
|
365
|
+
* title falls to the caller's key, the description to the raw backend `message`. An unknown
|
|
366
|
+
* reason (not in the map) gets the same generic title + raw-message fallback.
|
|
367
|
+
*/
|
|
368
|
+
function presentMappedConflict(conflict: ParsedConflict, fallbackTitleKey: string): void {
|
|
369
|
+
const info = conflict.reason
|
|
370
|
+
? CONFLICT_INFO[conflict.reason as Exclude<ConflictReason, BespokeConflictReason>]
|
|
371
|
+
: undefined
|
|
372
|
+
if (info) {
|
|
324
373
|
toast.add({
|
|
325
|
-
title: t(
|
|
326
|
-
description:
|
|
327
|
-
? t(
|
|
374
|
+
title: te(info.titleKey) ? t(info.titleKey) : t(fallbackTitleKey),
|
|
375
|
+
description: te(info.descriptionKey)
|
|
376
|
+
? t(info.descriptionKey)
|
|
328
377
|
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
329
|
-
color: '
|
|
330
|
-
icon: 'i-lucide-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
378
|
+
color: 'warning',
|
|
379
|
+
icon: 'i-lucide-triangle-alert',
|
|
380
|
+
// A reason with a jump action becomes an actionable, sticky toast (like the bespoke
|
|
381
|
+
// conflicts above) so the one-click remedy doesn't auto-dismiss before it's reached.
|
|
382
|
+
...(info.action
|
|
383
|
+
? {
|
|
384
|
+
duration: 0,
|
|
385
|
+
actions: [
|
|
386
|
+
{
|
|
387
|
+
label: t(info.action.labelKey),
|
|
388
|
+
icon: info.action.icon,
|
|
389
|
+
onClick: () => info.action?.run(ui),
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
}
|
|
393
|
+
: {}),
|
|
339
394
|
})
|
|
340
395
|
return
|
|
341
396
|
}
|
|
397
|
+
toast.add({
|
|
398
|
+
title: t(fallbackTitleKey),
|
|
399
|
+
description: conflict.message ?? t('errors.conflict.fallbackMessage'),
|
|
400
|
+
color: 'warning',
|
|
401
|
+
icon: 'i-lucide-triangle-alert',
|
|
402
|
+
})
|
|
403
|
+
}
|
|
342
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Present `error` as a toast. `fallbackTitleKey` is an i18n message key used for
|
|
407
|
+
* non-conflict failures and any conflict reason without a dedicated title.
|
|
408
|
+
*/
|
|
409
|
+
function present(error: unknown, fallbackTitleKey = 'common.actionFailed'): void {
|
|
410
|
+
const conflict = parseConflict(error)
|
|
343
411
|
if (conflict) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// so a key missing from the active locale falls back rather than leaking a raw key: the
|
|
347
|
-
// title falls to the caller's key, the description to the raw backend `message`. An unknown
|
|
348
|
-
// reason (not in the map) gets the same generic title + raw-message fallback.
|
|
349
|
-
const info = conflict.reason
|
|
350
|
-
? CONFLICT_INFO[conflict.reason as Exclude<ConflictReason, BespokeConflictReason>]
|
|
351
|
-
: undefined
|
|
352
|
-
if (info) {
|
|
353
|
-
toast.add({
|
|
354
|
-
title: te(info.titleKey) ? t(info.titleKey) : t(fallbackTitleKey),
|
|
355
|
-
description: te(info.descriptionKey)
|
|
356
|
-
? t(info.descriptionKey)
|
|
357
|
-
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
358
|
-
color: 'warning',
|
|
359
|
-
icon: 'i-lucide-triangle-alert',
|
|
360
|
-
// A reason with a jump action becomes an actionable, sticky toast (like the bespoke
|
|
361
|
-
// conflicts above) so the one-click remedy doesn't auto-dismiss before it's reached.
|
|
362
|
-
...(info.action
|
|
363
|
-
? {
|
|
364
|
-
duration: 0,
|
|
365
|
-
actions: [
|
|
366
|
-
{
|
|
367
|
-
label: t(info.action.labelKey),
|
|
368
|
-
icon: info.action.icon,
|
|
369
|
-
onClick: () => info.action?.run(ui),
|
|
370
|
-
},
|
|
371
|
-
],
|
|
372
|
-
}
|
|
373
|
-
: {}),
|
|
374
|
-
})
|
|
375
|
-
return
|
|
376
|
-
}
|
|
377
|
-
toast.add({
|
|
378
|
-
title: t(fallbackTitleKey),
|
|
379
|
-
description: conflict.message ?? t('errors.conflict.fallbackMessage'),
|
|
380
|
-
color: 'warning',
|
|
381
|
-
icon: 'i-lucide-triangle-alert',
|
|
382
|
-
})
|
|
412
|
+
if (presentBespokeConflict(conflict)) return
|
|
413
|
+
presentMappedConflict(conflict, fallbackTitleKey)
|
|
383
414
|
return
|
|
384
415
|
}
|
|
385
416
|
|
package/app/stores/apiKeys.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
|
-
import type { AddApiKeyInput, ApiKey, ApiKeyProvider } from '~/types/domain'
|
|
3
|
+
import type { AddApiKeyInput, ApiKey, ApiKeyProvider, UpdateApiKeyInput } from '~/types/domain'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The direct-provider API keys reachable from a workspace: the workspace's own keys
|
|
@@ -48,12 +48,30 @@ export const useApiKeysStore = defineStore('apiKeys', () => {
|
|
|
48
48
|
workspaceKeys.value = workspaceKeys.value.filter((k) => k.id !== id)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Pinning a default clears any prior default of the same scope+provider server-side, so a
|
|
52
|
+
// default change reloads the affected scope to reflect the single-default invariant; a plain
|
|
53
|
+
// enable/disable patches the one row in place.
|
|
54
|
+
async function updateWorkspaceKey(id: string, patch: UpdateApiKeyInput) {
|
|
55
|
+
if (!workspaceId.value) return
|
|
56
|
+
const updated = await api.updateWorkspaceApiKey(workspaceId.value, id, patch)
|
|
57
|
+
if (patch.isDefault !== undefined) await load(workspaceId.value)
|
|
58
|
+
else workspaceKeys.value = workspaceKeys.value.map((k) => (k.id === id ? updated : k))
|
|
59
|
+
return updated
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
async function addUserKey(input: AddApiKeyInput) {
|
|
52
63
|
const created = await api.addMyApiKey(input)
|
|
53
64
|
userKeys.value = [...userKeys.value, created]
|
|
54
65
|
return created
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
async function updateUserKey(id: string, patch: UpdateApiKeyInput) {
|
|
69
|
+
const updated = await api.updateMyApiKey(id, patch)
|
|
70
|
+
if (patch.isDefault !== undefined && workspaceId.value) await load(workspaceId.value)
|
|
71
|
+
else userKeys.value = userKeys.value.map((k) => (k.id === id ? updated : k))
|
|
72
|
+
return updated
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
async function removeUserKey(id: string) {
|
|
58
76
|
await api.removeMyApiKey(id)
|
|
59
77
|
userKeys.value = userKeys.value.filter((k) => k.id !== id)
|
|
@@ -72,6 +90,14 @@ export const useApiKeysStore = defineStore('apiKeys', () => {
|
|
|
72
90
|
accountKeys.value = [...accountKeys.value, created]
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
async function updateAccountKey(id: string, patch: UpdateApiKeyInput) {
|
|
94
|
+
if (!accountId.value) return
|
|
95
|
+
const updated = await api.updateAccountApiKey(accountId.value, id, patch)
|
|
96
|
+
if (patch.isDefault !== undefined) await loadAccountKeys(accountId.value)
|
|
97
|
+
else accountKeys.value = accountKeys.value.map((k) => (k.id === id ? updated : k))
|
|
98
|
+
return updated
|
|
99
|
+
}
|
|
100
|
+
|
|
75
101
|
async function removeAccountKey(id: string) {
|
|
76
102
|
if (!accountId.value) return
|
|
77
103
|
await api.removeAccountApiKey(accountId.value, id)
|
|
@@ -98,11 +124,14 @@ export const useApiKeysStore = defineStore('apiKeys', () => {
|
|
|
98
124
|
loading,
|
|
99
125
|
load,
|
|
100
126
|
addWorkspaceKey,
|
|
127
|
+
updateWorkspaceKey,
|
|
101
128
|
removeWorkspaceKey,
|
|
102
129
|
addUserKey,
|
|
130
|
+
updateUserKey,
|
|
103
131
|
removeUserKey,
|
|
104
132
|
loadAccountKeys,
|
|
105
133
|
addAccountKey,
|
|
134
|
+
updateAccountKey,
|
|
106
135
|
removeAccountKey,
|
|
107
136
|
configuredProviders,
|
|
108
137
|
hasProvider,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
SubscriptionVendor,
|
|
5
|
+
UpdateVendorCredentialInput,
|
|
6
|
+
VendorCredential,
|
|
7
|
+
} from '~/types/domain'
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* The workspace's connected LLM-vendor subscription credentials (the token pool
|
|
@@ -33,6 +37,22 @@ export const useVendorCredentialsStore = defineStore('vendorCredentials', () =>
|
|
|
33
37
|
credentials.value = [...credentials.value, created]
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Enable/disable and/or (un)pin a token as its vendor's default. Because pinning a
|
|
42
|
+
* default clears any prior default of the same vendor server-side, we reload the pool so
|
|
43
|
+
* the whole vendor group reflects the single-default invariant rather than patching one row.
|
|
44
|
+
*/
|
|
45
|
+
async function update(id: string, patch: UpdateVendorCredentialInput) {
|
|
46
|
+
if (!workspaceId.value) return
|
|
47
|
+
const updated = await api.updateVendorCredential(workspaceId.value, id, patch)
|
|
48
|
+
if (patch.isDefault !== undefined) {
|
|
49
|
+
await load(workspaceId.value)
|
|
50
|
+
} else {
|
|
51
|
+
credentials.value = credentials.value.map((c) => (c.id === id ? updated : c))
|
|
52
|
+
}
|
|
53
|
+
return updated
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
async function remove(id: string) {
|
|
37
57
|
if (!workspaceId.value) return
|
|
38
58
|
await api.removeVendorCredential(workspaceId.value, id)
|
|
@@ -50,5 +70,15 @@ export const useVendorCredentialsStore = defineStore('vendorCredentials', () =>
|
|
|
50
70
|
return credentials.value.filter((c) => c.vendor === vendor)
|
|
51
71
|
}
|
|
52
72
|
|
|
53
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
credentials,
|
|
75
|
+
loading,
|
|
76
|
+
load,
|
|
77
|
+
add,
|
|
78
|
+
update,
|
|
79
|
+
remove,
|
|
80
|
+
configuredVendors,
|
|
81
|
+
hasVendor,
|
|
82
|
+
forVendor,
|
|
83
|
+
}
|
|
54
84
|
})
|
package/app/types/models.ts
CHANGED
package/i18n/locales/de.json
CHANGED
|
@@ -2452,7 +2452,8 @@
|
|
|
2452
2452
|
"toast": {
|
|
2453
2453
|
"connected": "API-Schlüssel verbunden",
|
|
2454
2454
|
"connectFailed": "Schlüssel konnte nicht verbunden werden",
|
|
2455
|
-
"removeFailed": "Schlüssel konnte nicht entfernt werden"
|
|
2455
|
+
"removeFailed": "Schlüssel konnte nicht entfernt werden",
|
|
2456
|
+
"updateFailed": "Schlüssel konnte nicht aktualisiert werden"
|
|
2456
2457
|
},
|
|
2457
2458
|
"providers": {
|
|
2458
2459
|
"openai": {
|
|
@@ -2491,7 +2492,11 @@
|
|
|
2491
2492
|
"step2": "Die Basis-URL des Gateways wird von Ihrem Deployment-Betreiber festgelegt (LITELLM_BASE_URL), nicht hier."
|
|
2492
2493
|
}
|
|
2493
2494
|
},
|
|
2494
|
-
"keyNoun": "diesen API-Schlüssel"
|
|
2495
|
+
"keyNoun": "diesen API-Schlüssel",
|
|
2496
|
+
"defaultBadge": "Standard",
|
|
2497
|
+
"pinDefault": "Als Standard festlegen",
|
|
2498
|
+
"disabledBadge": "Deaktiviert",
|
|
2499
|
+
"enableToggle": "Diesen Schlüssel aktivieren oder deaktivieren"
|
|
2495
2500
|
},
|
|
2496
2501
|
"vendorCredentials": {
|
|
2497
2502
|
"title": "LLM-Vendors",
|
|
@@ -2514,7 +2519,8 @@
|
|
|
2514
2519
|
"toast": {
|
|
2515
2520
|
"connected": "Token verbunden",
|
|
2516
2521
|
"connectFailed": "Token konnte nicht verbunden werden",
|
|
2517
|
-
"removeFailed": "Token konnte nicht entfernt werden"
|
|
2522
|
+
"removeFailed": "Token konnte nicht entfernt werden",
|
|
2523
|
+
"updateFailed": "Token konnte nicht aktualisiert werden"
|
|
2518
2524
|
},
|
|
2519
2525
|
"vendors": {
|
|
2520
2526
|
"kimi": {
|
|
@@ -2533,7 +2539,11 @@
|
|
|
2533
2539
|
"confirmRemove": {
|
|
2534
2540
|
"title": "Diese Zugangsdaten entfernen?",
|
|
2535
2541
|
"body": "\"{name}\" wird entfernt. Dies kann nicht rückgängig gemacht werden."
|
|
2536
|
-
}
|
|
2542
|
+
},
|
|
2543
|
+
"defaultBadge": "Standard",
|
|
2544
|
+
"pinDefault": "Als Standard festlegen",
|
|
2545
|
+
"disabledBadge": "Deaktiviert",
|
|
2546
|
+
"enableToggle": "Dieses Token aktivieren oder deaktivieren"
|
|
2537
2547
|
}
|
|
2538
2548
|
},
|
|
2539
2549
|
"github": {
|
package/i18n/locales/en.json
CHANGED
|
@@ -2979,7 +2979,8 @@
|
|
|
2979
2979
|
"toast": {
|
|
2980
2980
|
"connected": "API key connected",
|
|
2981
2981
|
"connectFailed": "Could not connect key",
|
|
2982
|
-
"removeFailed": "Could not remove key"
|
|
2982
|
+
"removeFailed": "Could not remove key",
|
|
2983
|
+
"updateFailed": "Could not update key"
|
|
2983
2984
|
},
|
|
2984
2985
|
"providers": {
|
|
2985
2986
|
"openai": {
|
|
@@ -3018,7 +3019,11 @@
|
|
|
3018
3019
|
"step2": "The gateway's base URL is set by your deployment operator (LITELLM_BASE_URL), not here."
|
|
3019
3020
|
}
|
|
3020
3021
|
},
|
|
3021
|
-
"keyNoun": "this API key"
|
|
3022
|
+
"keyNoun": "this API key",
|
|
3023
|
+
"defaultBadge": "Default",
|
|
3024
|
+
"pinDefault": "Set as default",
|
|
3025
|
+
"disabledBadge": "Disabled",
|
|
3026
|
+
"enableToggle": "Enable or disable this key"
|
|
3022
3027
|
},
|
|
3023
3028
|
"vendorCredentials": {
|
|
3024
3029
|
"title": "LLM Vendors",
|
|
@@ -3044,7 +3049,8 @@
|
|
|
3044
3049
|
"toast": {
|
|
3045
3050
|
"connected": "Token connected",
|
|
3046
3051
|
"connectFailed": "Could not connect token",
|
|
3047
|
-
"removeFailed": "Could not remove token"
|
|
3052
|
+
"removeFailed": "Could not remove token",
|
|
3053
|
+
"updateFailed": "Could not update token"
|
|
3048
3054
|
},
|
|
3049
3055
|
"vendors": {
|
|
3050
3056
|
"kimi": {
|
|
@@ -3063,7 +3069,11 @@
|
|
|
3063
3069
|
"confirmRemove": {
|
|
3064
3070
|
"title": "Remove this credential?",
|
|
3065
3071
|
"body": "\"{name}\" will be removed. This can't be undone."
|
|
3066
|
-
}
|
|
3072
|
+
},
|
|
3073
|
+
"defaultBadge": "Default",
|
|
3074
|
+
"pinDefault": "Set as default",
|
|
3075
|
+
"disabledBadge": "Disabled",
|
|
3076
|
+
"enableToggle": "Enable or disable this token"
|
|
3067
3077
|
}
|
|
3068
3078
|
},
|
|
3069
3079
|
"github": {
|