@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
|
@@ -210,6 +210,23 @@ async function add() {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/** Route an update to the scope the key lives in (account / workspace / user). */
|
|
214
|
+
async function updateKey(k: ApiKey, patch: { enabled?: boolean; isDefault?: boolean }) {
|
|
215
|
+
try {
|
|
216
|
+
if (k.scope === 'account') await keys.updateAccountKey(k.id, patch)
|
|
217
|
+
else if (k.scope === 'workspace') await keys.updateWorkspaceKey(k.id, patch)
|
|
218
|
+
else await keys.updateUserKey(k.id, patch)
|
|
219
|
+
// Enabling/disabling changes provider selectability in the picker — refresh it.
|
|
220
|
+
if (workspace.workspaceId) await models.refresh(workspace.workspaceId)
|
|
221
|
+
} catch (e) {
|
|
222
|
+
toast.add({
|
|
223
|
+
title: t('providers.apiKeys.toast.updateFailed'),
|
|
224
|
+
description: e instanceof Error ? e.message : String(e),
|
|
225
|
+
color: 'error',
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
213
230
|
async function remove(k: ApiKey) {
|
|
214
231
|
const noun = t('providers.apiKeys.keyNoun')
|
|
215
232
|
if (!(await confirmAction('remove', noun))) return
|
|
@@ -352,10 +369,14 @@ async function remove(k: ApiKey) {
|
|
|
352
369
|
v-for="k in connected"
|
|
353
370
|
:key="k.id"
|
|
354
371
|
class="flex items-center justify-between rounded-md border border-slate-700 bg-slate-900/50 px-3 py-2 text-sm"
|
|
372
|
+
:class="{ 'opacity-55': !k.enabled }"
|
|
355
373
|
>
|
|
356
374
|
<div>
|
|
357
375
|
<span class="font-medium text-slate-200">{{ k.label }}</span>
|
|
358
376
|
<span class="ms-2 text-xs text-slate-500">{{ providerLabel(k.provider) }}</span>
|
|
377
|
+
<UBadge v-if="!k.enabled" color="neutral" variant="subtle" size="sm" class="ms-2">
|
|
378
|
+
{{ t('providers.apiKeys.disabledBadge') }}
|
|
379
|
+
</UBadge>
|
|
359
380
|
<div class="text-[11px] tabular-nums text-slate-500">
|
|
360
381
|
{{
|
|
361
382
|
t(
|
|
@@ -366,13 +387,32 @@ async function remove(k: ApiKey) {
|
|
|
366
387
|
}}
|
|
367
388
|
</div>
|
|
368
389
|
</div>
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
390
|
+
<div class="flex items-center gap-2">
|
|
391
|
+
<UButton
|
|
392
|
+
:icon="k.isDefault ? 'i-lucide-star' : 'i-lucide-star-off'"
|
|
393
|
+
:color="k.isDefault ? 'primary' : 'neutral'"
|
|
394
|
+
:variant="k.isDefault ? 'subtle' : 'ghost'"
|
|
395
|
+
size="xs"
|
|
396
|
+
@click="updateKey(k, { isDefault: !k.isDefault })"
|
|
397
|
+
>
|
|
398
|
+
{{
|
|
399
|
+
k.isDefault ? t('providers.apiKeys.defaultBadge') : t('providers.apiKeys.pinDefault')
|
|
400
|
+
}}
|
|
401
|
+
</UButton>
|
|
402
|
+
<USwitch
|
|
403
|
+
:model-value="k.enabled"
|
|
404
|
+
size="sm"
|
|
405
|
+
:aria-label="t('providers.apiKeys.enableToggle')"
|
|
406
|
+
@update:model-value="(v: boolean) => updateKey(k, { enabled: v })"
|
|
407
|
+
/>
|
|
408
|
+
<UButton
|
|
409
|
+
icon="i-lucide-trash-2"
|
|
410
|
+
color="error"
|
|
411
|
+
variant="ghost"
|
|
412
|
+
size="xs"
|
|
413
|
+
@click="remove(k)"
|
|
414
|
+
/>
|
|
415
|
+
</div>
|
|
376
416
|
</div>
|
|
377
417
|
</div>
|
|
378
418
|
</div>
|
|
@@ -141,6 +141,30 @@ async function add() {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
async function toggleEnabled(cred: { id: string }, enabled: boolean) {
|
|
145
|
+
try {
|
|
146
|
+
await creds.update(cred.id, { enabled })
|
|
147
|
+
} catch (e) {
|
|
148
|
+
toast.add({
|
|
149
|
+
title: t('providers.vendorCredentials.toast.updateFailed'),
|
|
150
|
+
description: e instanceof Error ? e.message : String(e),
|
|
151
|
+
color: 'error',
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function toggleDefault(cred: { id: string; isDefault: boolean }) {
|
|
157
|
+
try {
|
|
158
|
+
await creds.update(cred.id, { isDefault: !cred.isDefault })
|
|
159
|
+
} catch (e) {
|
|
160
|
+
toast.add({
|
|
161
|
+
title: t('providers.vendorCredentials.toast.updateFailed'),
|
|
162
|
+
description: e instanceof Error ? e.message : String(e),
|
|
163
|
+
color: 'error',
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
async function remove(cred: { id: string; label: string }) {
|
|
145
169
|
const ok = await confirm({
|
|
146
170
|
title: t('providers.vendorCredentials.confirmRemove.title'),
|
|
@@ -245,10 +269,14 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
245
269
|
v-for="c in creds.credentials"
|
|
246
270
|
:key="c.id"
|
|
247
271
|
class="flex items-center justify-between rounded-md border border-slate-700 bg-slate-900/50 px-3 py-2 text-sm"
|
|
272
|
+
:class="{ 'opacity-55': !c.enabled }"
|
|
248
273
|
>
|
|
249
274
|
<div>
|
|
250
275
|
<span class="font-medium text-slate-200">{{ c.label }}</span>
|
|
251
276
|
<span class="ms-2 text-xs text-slate-500">{{ vendorLabel(c.vendor) }}</span>
|
|
277
|
+
<UBadge v-if="!c.enabled" color="neutral" variant="subtle" size="sm" class="ms-2">
|
|
278
|
+
{{ t('providers.vendorCredentials.disabledBadge') }}
|
|
279
|
+
</UBadge>
|
|
252
280
|
<div class="text-[11px] tabular-nums text-slate-500">
|
|
253
281
|
{{
|
|
254
282
|
t(
|
|
@@ -262,13 +290,34 @@ function vendorLabel(v: SubscriptionVendor): string {
|
|
|
262
290
|
}}
|
|
263
291
|
</div>
|
|
264
292
|
</div>
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
293
|
+
<div class="flex items-center gap-2">
|
|
294
|
+
<UButton
|
|
295
|
+
:icon="c.isDefault ? 'i-lucide-star' : 'i-lucide-star-off'"
|
|
296
|
+
:color="c.isDefault ? 'primary' : 'neutral'"
|
|
297
|
+
:variant="c.isDefault ? 'subtle' : 'ghost'"
|
|
298
|
+
size="xs"
|
|
299
|
+
@click="toggleDefault(c)"
|
|
300
|
+
>
|
|
301
|
+
{{
|
|
302
|
+
c.isDefault
|
|
303
|
+
? t('providers.vendorCredentials.defaultBadge')
|
|
304
|
+
: t('providers.vendorCredentials.pinDefault')
|
|
305
|
+
}}
|
|
306
|
+
</UButton>
|
|
307
|
+
<USwitch
|
|
308
|
+
:model-value="c.enabled"
|
|
309
|
+
size="sm"
|
|
310
|
+
:aria-label="t('providers.vendorCredentials.enableToggle')"
|
|
311
|
+
@update:model-value="(v: boolean) => toggleEnabled(c, v)"
|
|
312
|
+
/>
|
|
313
|
+
<UButton
|
|
314
|
+
icon="i-lucide-trash-2"
|
|
315
|
+
color="error"
|
|
316
|
+
variant="ghost"
|
|
317
|
+
size="xs"
|
|
318
|
+
@click="remove(c)"
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
272
321
|
</div>
|
|
273
322
|
</div>
|
|
274
323
|
</div>
|
|
@@ -77,6 +77,44 @@ const schemeItems = computed(() => [
|
|
|
77
77
|
{ label: 'http', value: 'http' },
|
|
78
78
|
])
|
|
79
79
|
|
|
80
|
+
// A stored non-secret value is `unknown`; coerce it to the string the form field holds,
|
|
81
|
+
// defaulting a missing/wrong-typed value to '' (the same `typeof … === 'string' ? … : ''`
|
|
82
|
+
// each field used inline before).
|
|
83
|
+
function readString(v: unknown): string {
|
|
84
|
+
return typeof v === 'string' ? v : ''
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Prefill the manifest-source fields from the stored discriminated config.
|
|
88
|
+
function applyManifestSource(k: Record<string, unknown>): void {
|
|
89
|
+
const src = k.manifestSource as Record<string, unknown> | undefined
|
|
90
|
+
if (src?.type === 'separate') {
|
|
91
|
+
form.manifestSourceType = 'separate'
|
|
92
|
+
form.manifestRepo = readString(src.repo)
|
|
93
|
+
form.manifestRef = readString(src.ref)
|
|
94
|
+
form.manifestPath = readString(src.path)
|
|
95
|
+
} else if (src?.type === 'colocated') {
|
|
96
|
+
form.manifestSourceType = 'colocated'
|
|
97
|
+
form.manifestPath = readString(src.path)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Prefill the URL-derivation fields (+ scheme) from the stored discriminated config.
|
|
102
|
+
function applyUrl(k: Record<string, unknown>): void {
|
|
103
|
+
const url = k.url as Record<string, unknown> | undefined
|
|
104
|
+
if (url?.source === 'ingressTemplate') {
|
|
105
|
+
form.urlSource = 'ingressTemplate'
|
|
106
|
+
form.hostTemplate = readString(url.hostTemplate)
|
|
107
|
+
} else if (url?.source === 'ingressStatus') {
|
|
108
|
+
form.urlSource = 'ingressStatus'
|
|
109
|
+
form.ingressName = readString(url.ingressName)
|
|
110
|
+
} else if (url?.source === 'serviceStatus') {
|
|
111
|
+
form.urlSource = 'serviceStatus'
|
|
112
|
+
form.serviceName = readString(url.serviceName)
|
|
113
|
+
form.servicePort = typeof url.port === 'number' ? String(url.port) : ''
|
|
114
|
+
}
|
|
115
|
+
form.urlScheme = url && (url.scheme === 'http' || url.scheme === 'https') ? url.scheme : 'default'
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
// A registered k8s env connection exposes its non-secret config, so prefill every
|
|
81
119
|
// non-secret field from it (never the token — secrets are write-only and re-entered on
|
|
82
120
|
// update). This lets an edit change one field without re-typing the whole form.
|
|
@@ -89,36 +127,14 @@ watch(
|
|
|
89
127
|
? (c.config as { kubernetes: Record<string, unknown> }).kubernetes
|
|
90
128
|
: undefined
|
|
91
129
|
if (!k) return
|
|
92
|
-
form.label =
|
|
93
|
-
form.apiServerUrl =
|
|
94
|
-
form.caCertPem =
|
|
130
|
+
form.label = readString(k.label)
|
|
131
|
+
form.apiServerUrl = readString(k.apiServerUrl)
|
|
132
|
+
form.caCertPem = readString(k.caCertPem)
|
|
95
133
|
form.insecureSkipTlsVerify = k.insecureSkipTlsVerify === true
|
|
96
|
-
form.namespaceTemplate =
|
|
97
|
-
form.imageTemplate =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
form.manifestSourceType = 'separate'
|
|
101
|
-
form.manifestRepo = typeof src.repo === 'string' ? src.repo : ''
|
|
102
|
-
form.manifestRef = typeof src.ref === 'string' ? src.ref : ''
|
|
103
|
-
form.manifestPath = typeof src.path === 'string' ? src.path : ''
|
|
104
|
-
} else if (src?.type === 'colocated') {
|
|
105
|
-
form.manifestSourceType = 'colocated'
|
|
106
|
-
form.manifestPath = typeof src.path === 'string' ? src.path : ''
|
|
107
|
-
}
|
|
108
|
-
const url = k.url as Record<string, unknown> | undefined
|
|
109
|
-
if (url?.source === 'ingressTemplate') {
|
|
110
|
-
form.urlSource = 'ingressTemplate'
|
|
111
|
-
form.hostTemplate = typeof url.hostTemplate === 'string' ? url.hostTemplate : ''
|
|
112
|
-
} else if (url?.source === 'ingressStatus') {
|
|
113
|
-
form.urlSource = 'ingressStatus'
|
|
114
|
-
form.ingressName = typeof url.ingressName === 'string' ? url.ingressName : ''
|
|
115
|
-
} else if (url?.source === 'serviceStatus') {
|
|
116
|
-
form.urlSource = 'serviceStatus'
|
|
117
|
-
form.serviceName = typeof url.serviceName === 'string' ? url.serviceName : ''
|
|
118
|
-
form.servicePort = typeof url.port === 'number' ? String(url.port) : ''
|
|
119
|
-
}
|
|
120
|
-
form.urlScheme =
|
|
121
|
-
url && (url.scheme === 'http' || url.scheme === 'https') ? url.scheme : 'default'
|
|
134
|
+
form.namespaceTemplate = readString(k.namespaceTemplate)
|
|
135
|
+
form.imageTemplate = readString(k.imageTemplate)
|
|
136
|
+
applyManifestSource(k)
|
|
137
|
+
applyUrl(k)
|
|
122
138
|
},
|
|
123
139
|
{ immediate: true },
|
|
124
140
|
)
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
setServiceFragmentDefaultsContract,
|
|
24
24
|
storePersonalSubscriptionContract,
|
|
25
25
|
testLocalModelEndpointContract,
|
|
26
|
+
updateAccountApiKeyContract,
|
|
27
|
+
updateUserApiKeyContract,
|
|
28
|
+
updateVendorCredentialContract,
|
|
29
|
+
updateWorkspaceApiKeyContract,
|
|
26
30
|
upsertLocalModelEndpointContract,
|
|
27
31
|
upsertOpenRouterCatalogContract,
|
|
28
32
|
} from '@cat-factory/contracts'
|
|
@@ -30,6 +34,8 @@ import type {
|
|
|
30
34
|
AddApiKeyInput,
|
|
31
35
|
StorePersonalSubscriptionInput,
|
|
32
36
|
SubscriptionVendor,
|
|
37
|
+
UpdateApiKeyInput,
|
|
38
|
+
UpdateVendorCredentialInput,
|
|
33
39
|
} from '~/types/domain'
|
|
34
40
|
import type {
|
|
35
41
|
LocalRunner,
|
|
@@ -60,16 +66,26 @@ export function modelsApi({ send, ws }: ApiContext) {
|
|
|
60
66
|
send(listWorkspaceApiKeysContract, { pathPrefix: ws(workspaceId) }),
|
|
61
67
|
addWorkspaceApiKey: (workspaceId: string, body: AddApiKeyInput) =>
|
|
62
68
|
send(addWorkspaceApiKeyContract, { pathPrefix: ws(workspaceId), body }),
|
|
69
|
+
updateWorkspaceApiKey: (workspaceId: string, id: string, body: UpdateApiKeyInput) =>
|
|
70
|
+
send(updateWorkspaceApiKeyContract, {
|
|
71
|
+
pathPrefix: ws(workspaceId),
|
|
72
|
+
pathParams: { id },
|
|
73
|
+
body,
|
|
74
|
+
}),
|
|
63
75
|
removeWorkspaceApiKey: (workspaceId: string, id: string) =>
|
|
64
76
|
send(removeWorkspaceApiKeyContract, { pathPrefix: ws(workspaceId), pathParams: { id } }),
|
|
65
77
|
listMyApiKeys: () => send(listUserApiKeysContract, {}),
|
|
66
78
|
addMyApiKey: (body: AddApiKeyInput) => send(addUserApiKeyContract, { body }),
|
|
79
|
+
updateMyApiKey: (id: string, body: UpdateApiKeyInput) =>
|
|
80
|
+
send(updateUserApiKeyContract, { pathParams: { id }, body }),
|
|
67
81
|
removeMyApiKey: (id: string) => send(removeUserApiKeyContract, { pathParams: { id } }),
|
|
68
82
|
// Account-scoped keys (shared by every workspace in the account); admin-only.
|
|
69
83
|
listAccountApiKeys: (accountId: string) =>
|
|
70
84
|
send(listAccountApiKeysContract, { pathParams: { accountId } }),
|
|
71
85
|
addAccountApiKey: (accountId: string, body: AddApiKeyInput) =>
|
|
72
86
|
send(addAccountApiKeyContract, { pathParams: { accountId }, body }),
|
|
87
|
+
updateAccountApiKey: (accountId: string, id: string, body: UpdateApiKeyInput) =>
|
|
88
|
+
send(updateAccountApiKeyContract, { pathParams: { accountId, id }, body }),
|
|
73
89
|
removeAccountApiKey: (accountId: string, id: string) =>
|
|
74
90
|
send(removeAccountApiKeyContract, { pathParams: { accountId, id } }),
|
|
75
91
|
|
|
@@ -80,6 +96,12 @@ export function modelsApi({ send, ws }: ApiContext) {
|
|
|
80
96
|
workspaceId: string,
|
|
81
97
|
body: { vendor: SubscriptionVendor; label: string; token: string },
|
|
82
98
|
) => send(addVendorCredentialContract, { pathPrefix: ws(workspaceId), body }),
|
|
99
|
+
updateVendorCredential: (workspaceId: string, id: string, body: UpdateVendorCredentialInput) =>
|
|
100
|
+
send(updateVendorCredentialContract, {
|
|
101
|
+
pathPrefix: ws(workspaceId),
|
|
102
|
+
pathParams: { id },
|
|
103
|
+
body,
|
|
104
|
+
}),
|
|
83
105
|
removeVendorCredential: (workspaceId: string, id: string) =>
|
|
84
106
|
send(removeVendorCredentialContract, { pathPrefix: ws(workspaceId), pathParams: { id } }),
|
|
85
107
|
|