@cat-factory/app 0.47.2 → 0.47.4
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/github/AddServiceFromRepoModal.vue +14 -12
- package/app/components/layout/IntegrationsHub.vue +44 -23
- package/app/components/settings/InfrastructureWindow.vue +221 -0
- package/app/components/settings/ProviderConnectionTab.vue +358 -0
- package/app/components/settings/ProviderManifestEditor.vue +286 -0
- package/app/pages/index.vue +3 -3
- package/app/stores/ui.ts +12 -6
- package/i18n/locales/en.json +23 -10
- package/i18n/locales/es.json +23 -10
- package/i18n/locales/fr.json +23 -10
- package/i18n/locales/pl.json +23 -10
- package/i18n/locales/uk.json +23 -10
- package/package.json +2 -1
- package/app/components/settings/ProviderConnectionPanel.vue +0 -524
|
@@ -1,524 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// The generic connect form for the two infrastructure providers — the ephemeral-environment
|
|
3
|
-
// provider and the self-hosted runner pool. Both self-describe via a ProviderDescriptor
|
|
4
|
-
// (fields + defaults + the missingRequired keys still owed); this renders them without
|
|
5
|
-
// hard-coding either. A NATIVE provider also ships a `manifestTemplate`, so the flat fields
|
|
6
|
-
// are overlaid back onto a full manifest before saving (the single manifest storage path —
|
|
7
|
-
// see backend/docs/native-environment-adapter.md): a `secret` field → the write-only secret
|
|
8
|
-
// bundle, a non-secret field → providerConfig[key], a `baseUrl` field → baseUrl. A field
|
|
9
|
-
// with a `default` is optional — left blank it falls back to that default.
|
|
10
|
-
import { computed, ref, toRaw, watch } from 'vue'
|
|
11
|
-
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
12
|
-
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
13
|
-
import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
|
|
14
|
-
|
|
15
|
-
const { t } = useI18n()
|
|
16
|
-
const ui = useUiStore()
|
|
17
|
-
const store = useProviderConnectionsStore()
|
|
18
|
-
const toast = useToast()
|
|
19
|
-
|
|
20
|
-
// Per-kind icon (display-only). The title + blurb copy resolve through the i18n catalog
|
|
21
|
-
// via literal keys keyed off the provider-connection kind (see TITLE_KEYS / BLURB_KEYS).
|
|
22
|
-
const ICONS: Record<ProviderConnectionKind, string> = {
|
|
23
|
-
environment: 'i-lucide-cloud',
|
|
24
|
-
'runner-pool': 'i-lucide-server-cog',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Exhaustive Record keyed off the provider-connection-kind union (a missing member fails
|
|
28
|
-
// the typecheck); each value is a LITERAL catalog key so the typed-message-keys check sees
|
|
29
|
-
// it. Leaf keys mirror the kind verbatim.
|
|
30
|
-
const TITLE_KEYS: Record<ProviderConnectionKind, string> = {
|
|
31
|
-
environment: 'settings.providerConnection.kind.environment.title',
|
|
32
|
-
'runner-pool': 'settings.providerConnection.kind.runner-pool.title',
|
|
33
|
-
}
|
|
34
|
-
const BLURB_KEYS: Record<ProviderConnectionKind, string> = {
|
|
35
|
-
environment: 'settings.providerConnection.kind.environment.blurb',
|
|
36
|
-
'runner-pool': 'settings.providerConnection.kind.runner-pool.blurb',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const kind = computed<ProviderConnectionKind | null>(() => ui.providerConnectionKind)
|
|
40
|
-
const open = computed({
|
|
41
|
-
get: () => kind.value !== null,
|
|
42
|
-
set: (v: boolean) => {
|
|
43
|
-
if (!v) ui.closeProviderConnection()
|
|
44
|
-
},
|
|
45
|
-
})
|
|
46
|
-
const back = useIntegrationBack(open)
|
|
47
|
-
|
|
48
|
-
const meta = computed(() =>
|
|
49
|
-
kind.value
|
|
50
|
-
? {
|
|
51
|
-
title: t(TITLE_KEYS[kind.value]),
|
|
52
|
-
icon: ICONS[kind.value],
|
|
53
|
-
blurb: t(BLURB_KEYS[kind.value]),
|
|
54
|
-
}
|
|
55
|
-
: null,
|
|
56
|
-
)
|
|
57
|
-
const descriptor = computed(() => (kind.value ? store.descriptorFor(kind.value) : null))
|
|
58
|
-
const connection = computed(() => (kind.value ? store.connectionFor(kind.value) : null))
|
|
59
|
-
|
|
60
|
-
// --- Local-mode infrastructure delegation -------------------------------------------
|
|
61
|
-
// In local mode this same screen is where a developer chooses, per workspace, whether to
|
|
62
|
-
// run on this machine (host Docker for agents, in-container docker-compose for the Tester)
|
|
63
|
-
// or delegate to an external service. The two opt-ins live here together to make the
|
|
64
|
-
// cross-cutting nature explicit: the environment provider you configure on this screen is
|
|
65
|
-
// one half; the runner pool (its own screen) is the other. Each toggle is enabled only
|
|
66
|
-
// once its provider is registered. Shown only in local mode and only on the environment
|
|
67
|
-
// kind (so it appears once, alongside the env provider it relates to).
|
|
68
|
-
const auth = useAuthStore()
|
|
69
|
-
const settings = useWorkspaceSettingsStore()
|
|
70
|
-
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
71
|
-
const showLocalDelegation = computed(() => isLocal.value && kind.value === 'environment')
|
|
72
|
-
// Gating: a toggle's external option is selectable only when its provider is registered.
|
|
73
|
-
const runnerPoolRegistered = computed(() => !!store.connectionFor('runner-pool'))
|
|
74
|
-
const envRegistered = computed(() => !!store.connectionFor('environment'))
|
|
75
|
-
const savingDelegation = ref(false)
|
|
76
|
-
|
|
77
|
-
async function setDelegation(patch: {
|
|
78
|
-
delegateAgentsToRunnerPool?: boolean
|
|
79
|
-
delegateTestEnvToProvider?: boolean
|
|
80
|
-
}) {
|
|
81
|
-
savingDelegation.value = true
|
|
82
|
-
try {
|
|
83
|
-
await settings.update(patch)
|
|
84
|
-
} catch (e) {
|
|
85
|
-
notifyError(t('settings.providerConnection.delegation.updateFailed'), e)
|
|
86
|
-
} finally {
|
|
87
|
-
savingDelegation.value = false
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function openRunnerPoolPanel() {
|
|
92
|
-
ui.openProviderConnection('runner-pool')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// "View logs": the provisioning event history for this provider's subsystem — every
|
|
96
|
-
// spin-up / tear-down attempt with its outcome and the exact error. The panel kind
|
|
97
|
-
// maps 1:1 to the log subsystem ('environment' / 'runner-pool').
|
|
98
|
-
const showLogs = ref(false)
|
|
99
|
-
watch(kind, () => {
|
|
100
|
-
showLogs.value = false
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// Per-field draft values, keyed by field key (blank ⇒ fall back to default/stored value).
|
|
104
|
-
const values = ref<Record<string, string>>({})
|
|
105
|
-
const testResult = ref<{ ok: boolean; message?: string } | null>(null)
|
|
106
|
-
const testing = ref(false)
|
|
107
|
-
const busy = ref(false)
|
|
108
|
-
|
|
109
|
-
// Seed the draft from the saved manifest so an edit starts from the CURRENT non-secret config
|
|
110
|
-
// (baseUrl + providerConfig) rather than blanks — re-saving then re-sends it instead of
|
|
111
|
-
// dropping it. Secret fields are never prefilled (write-only); they must be re-entered to save.
|
|
112
|
-
function resetDraft() {
|
|
113
|
-
testResult.value = null
|
|
114
|
-
const saved = descriptor.value?.savedManifest
|
|
115
|
-
const cfg = (saved?.providerConfig as Record<string, unknown> | undefined) ?? {}
|
|
116
|
-
const next: Record<string, string> = {}
|
|
117
|
-
for (const f of descriptor.value?.configFields ?? []) {
|
|
118
|
-
if (f.secret) continue
|
|
119
|
-
if (f.key === 'baseUrl') {
|
|
120
|
-
const b = saved?.baseUrl ?? connection.value?.baseUrl
|
|
121
|
-
if (typeof b === 'string') next[f.key] = b
|
|
122
|
-
} else if (typeof cfg[f.key] === 'string') {
|
|
123
|
-
next[f.key] = cfg[f.key] as string
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
values.value = next
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
watch(
|
|
130
|
-
kind,
|
|
131
|
-
(k) => {
|
|
132
|
-
if (k) void store.loadKind(k).then(resetDraft)
|
|
133
|
-
// In local mode the env panel also gates the agents toggle on a registered runner
|
|
134
|
-
// pool, so load that provider's connection state too (the env kind already loads above).
|
|
135
|
-
if (k === 'environment' && isLocal.value) void store.loadKind('runner-pool')
|
|
136
|
-
},
|
|
137
|
-
{ immediate: true },
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
/** A native provider ships a manifest scaffold ⇒ we can author/register the full manifest. */
|
|
141
|
-
const canAuthor = computed(() => !!descriptor.value?.manifestTemplate)
|
|
142
|
-
const secretFieldCount = computed(
|
|
143
|
-
() => (descriptor.value?.configFields ?? []).filter((f) => f.secret).length,
|
|
144
|
-
)
|
|
145
|
-
const hasSecretFields = computed(() => secretFieldCount.value > 0)
|
|
146
|
-
/** Already-configured manifest provider: we can still rotate its secrets. */
|
|
147
|
-
const canRotateSecrets = computed(() => !canAuthor.value && !!connection.value)
|
|
148
|
-
|
|
149
|
-
/** A field is satisfied when filled now, or already stored, or it has a default. */
|
|
150
|
-
function satisfied(key: string): boolean {
|
|
151
|
-
const f = descriptor.value?.configFields.find((cf) => cf.key === key)
|
|
152
|
-
if (!f) return true
|
|
153
|
-
if ((values.value[key] ?? '').trim()) return true
|
|
154
|
-
if (f.default !== undefined) return true
|
|
155
|
-
return !(descriptor.value?.missingRequired ?? []).includes(key)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Save is allowed once every required-without-default key is supplied. */
|
|
159
|
-
const canSave = computed(() => {
|
|
160
|
-
if (!descriptor.value) return false
|
|
161
|
-
if (!canAuthor.value && !canRotateSecrets.value) return false
|
|
162
|
-
return (descriptor.value.missingRequired ?? []).every(satisfied)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Overlay the flat field values onto the provider's manifest. We base the overlay on the
|
|
167
|
-
* CURRENT saved manifest when one exists (so previously-stored providerConfig — including
|
|
168
|
-
* nested values the flat form doesn't render — survives a re-save), falling back to the bare
|
|
169
|
-
* `manifestTemplate` scaffold on a first connect. Native providers only (a manifest provider
|
|
170
|
-
* has no template ⇒ null, and rotates secrets via the dedicated path instead).
|
|
171
|
-
*/
|
|
172
|
-
function buildManifestPayload(): {
|
|
173
|
-
manifest: Record<string, unknown>
|
|
174
|
-
secrets: Record<string, string>
|
|
175
|
-
} | null {
|
|
176
|
-
const template = descriptor.value?.manifestTemplate
|
|
177
|
-
if (!template) return null
|
|
178
|
-
const base = descriptor.value?.savedManifest ?? template
|
|
179
|
-
// `base` is a Vue reactive proxy, which structuredClone refuses (DataCloneError). `toRaw`
|
|
180
|
-
// unwraps it to the underlying plain-JSON config so structuredClone can deep-clone it.
|
|
181
|
-
const manifest: Record<string, unknown> = structuredClone(toRaw(base))
|
|
182
|
-
const providerConfig: Record<string, unknown> = {
|
|
183
|
-
...(manifest.providerConfig as Record<string, unknown> | undefined),
|
|
184
|
-
}
|
|
185
|
-
const secrets: Record<string, string> = {}
|
|
186
|
-
for (const f of descriptor.value?.configFields ?? []) {
|
|
187
|
-
const val = (values.value[f.key] ?? '').trim()
|
|
188
|
-
if (!val) continue // omit ⇒ falls back to the scaffold default
|
|
189
|
-
if (f.secret) secrets[f.key] = val
|
|
190
|
-
else if (f.key === 'baseUrl') manifest.baseUrl = val
|
|
191
|
-
else providerConfig[f.key] = val
|
|
192
|
-
}
|
|
193
|
-
if (Object.keys(providerConfig).length) manifest.providerConfig = providerConfig
|
|
194
|
-
return { manifest, secrets }
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Just the secret-field values (for rotating an authored manifest provider's secrets). */
|
|
198
|
-
function buildSecretsOnly(): Record<string, string> {
|
|
199
|
-
const secrets: Record<string, string> = {}
|
|
200
|
-
for (const f of descriptor.value?.configFields ?? []) {
|
|
201
|
-
if (!f.secret) continue
|
|
202
|
-
const val = (values.value[f.key] ?? '').trim()
|
|
203
|
-
if (val) secrets[f.key] = val
|
|
204
|
-
}
|
|
205
|
-
return secrets
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function notifyError(title: string, e: unknown) {
|
|
209
|
-
toast.add({
|
|
210
|
-
title,
|
|
211
|
-
description: e instanceof Error ? e.message : String(e),
|
|
212
|
-
icon: 'i-lucide-triangle-alert',
|
|
213
|
-
color: 'error',
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function test() {
|
|
218
|
-
if (!kind.value) return
|
|
219
|
-
const payload = buildManifestPayload()
|
|
220
|
-
testing.value = true
|
|
221
|
-
testResult.value = null
|
|
222
|
-
try {
|
|
223
|
-
testResult.value = await store.test(kind.value, payload ?? { secrets: buildSecretsOnly() })
|
|
224
|
-
} catch (e) {
|
|
225
|
-
testResult.value = { ok: false, message: e instanceof Error ? e.message : String(e) }
|
|
226
|
-
} finally {
|
|
227
|
-
testing.value = false
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function save() {
|
|
232
|
-
if (!kind.value) return
|
|
233
|
-
busy.value = true
|
|
234
|
-
try {
|
|
235
|
-
if (canAuthor.value) {
|
|
236
|
-
const payload = buildManifestPayload()
|
|
237
|
-
if (payload) await store.register(kind.value, payload)
|
|
238
|
-
} else {
|
|
239
|
-
await store.updateSecrets(kind.value, buildSecretsOnly())
|
|
240
|
-
}
|
|
241
|
-
resetDraft()
|
|
242
|
-
toast.add({
|
|
243
|
-
title: t('settings.providerConnection.toast.saved', { title: meta.value?.title ?? '' }),
|
|
244
|
-
icon: 'i-lucide-check',
|
|
245
|
-
color: 'success',
|
|
246
|
-
})
|
|
247
|
-
} catch (e) {
|
|
248
|
-
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
249
|
-
} finally {
|
|
250
|
-
busy.value = false
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function remove() {
|
|
255
|
-
if (!kind.value) return
|
|
256
|
-
busy.value = true
|
|
257
|
-
try {
|
|
258
|
-
await store.remove(kind.value)
|
|
259
|
-
resetDraft()
|
|
260
|
-
toast.add({ title: t('settings.providerConnection.toast.removed'), icon: 'i-lucide-check' })
|
|
261
|
-
} catch (e) {
|
|
262
|
-
notifyError(t('settings.providerConnection.toast.removeFailed'), e)
|
|
263
|
-
} finally {
|
|
264
|
-
busy.value = false
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** The helper line under a field — its own help, plus the "defaulted to …" hint. */
|
|
269
|
-
function fieldHelp(key: string): string | undefined {
|
|
270
|
-
const f = descriptor.value?.configFields.find((cf) => cf.key === key)
|
|
271
|
-
if (!f) return undefined
|
|
272
|
-
const filled = (values.value[key] ?? '').trim()
|
|
273
|
-
if (f.default !== undefined && !filled) {
|
|
274
|
-
const defaulted = t('settings.providerConnection.field.defaultsTo', { value: f.default })
|
|
275
|
-
return f.help ? `${f.help} · ${defaulted}` : defaulted
|
|
276
|
-
}
|
|
277
|
-
return f.help
|
|
278
|
-
}
|
|
279
|
-
</script>
|
|
280
|
-
|
|
281
|
-
<template>
|
|
282
|
-
<UModal
|
|
283
|
-
v-model:open="open"
|
|
284
|
-
:title="meta?.title ?? t('settings.providerConnection.fallbackTitle')"
|
|
285
|
-
:ui="{ content: 'max-w-xl' }"
|
|
286
|
-
>
|
|
287
|
-
<template #title>
|
|
288
|
-
<IntegrationBackTitle
|
|
289
|
-
:title="meta?.title ?? t('settings.providerConnection.fallbackTitle')"
|
|
290
|
-
@back="back"
|
|
291
|
-
/>
|
|
292
|
-
</template>
|
|
293
|
-
<template #body>
|
|
294
|
-
<!-- Local-mode infrastructure delegation: the local-vs-external choice for BOTH
|
|
295
|
-
container agents AND the Tester's ephemeral environments, made once here. -->
|
|
296
|
-
<section
|
|
297
|
-
v-if="showLocalDelegation"
|
|
298
|
-
class="mb-4 space-y-3 rounded-lg border border-slate-700 bg-slate-900/40 p-3"
|
|
299
|
-
>
|
|
300
|
-
<div>
|
|
301
|
-
<h3 class="text-sm font-semibold text-slate-200">
|
|
302
|
-
{{ t('settings.providerConnection.delegation.title') }}
|
|
303
|
-
</h3>
|
|
304
|
-
<p class="mt-1 text-[11px] text-slate-400">
|
|
305
|
-
{{ t('settings.providerConnection.delegation.intro') }}
|
|
306
|
-
</p>
|
|
307
|
-
</div>
|
|
308
|
-
|
|
309
|
-
<!-- Container agents → self-hosted runner pool -->
|
|
310
|
-
<div class="space-y-1">
|
|
311
|
-
<label class="flex items-center gap-2">
|
|
312
|
-
<USwitch
|
|
313
|
-
size="sm"
|
|
314
|
-
:model-value="settings.settings.delegateAgentsToRunnerPool"
|
|
315
|
-
:disabled="savingDelegation || !runnerPoolRegistered"
|
|
316
|
-
@update:model-value="(v) => setDelegation({ delegateAgentsToRunnerPool: v })"
|
|
317
|
-
/>
|
|
318
|
-
<span class="text-sm text-slate-200">
|
|
319
|
-
{{ t('settings.providerConnection.delegation.agentsToggle') }}
|
|
320
|
-
</span>
|
|
321
|
-
</label>
|
|
322
|
-
<p class="pl-9 text-[11px] text-slate-400">
|
|
323
|
-
{{ t('settings.providerConnection.delegation.agentsHint') }}
|
|
324
|
-
<template v-if="!runnerPoolRegistered">
|
|
325
|
-
<i18n-t
|
|
326
|
-
keypath="settings.providerConnection.delegation.registerPoolPrompt"
|
|
327
|
-
tag="span"
|
|
328
|
-
scope="global"
|
|
329
|
-
>
|
|
330
|
-
<template #link>
|
|
331
|
-
<button
|
|
332
|
-
type="button"
|
|
333
|
-
class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
|
|
334
|
-
@click="openRunnerPoolPanel"
|
|
335
|
-
>
|
|
336
|
-
{{ t('settings.providerConnection.delegation.registerPoolLink') }}
|
|
337
|
-
</button>
|
|
338
|
-
</template>
|
|
339
|
-
</i18n-t>
|
|
340
|
-
</template>
|
|
341
|
-
</p>
|
|
342
|
-
</div>
|
|
343
|
-
|
|
344
|
-
<!-- Tester environments → environment provider -->
|
|
345
|
-
<div class="space-y-1">
|
|
346
|
-
<label class="flex items-center gap-2">
|
|
347
|
-
<USwitch
|
|
348
|
-
size="sm"
|
|
349
|
-
:model-value="settings.settings.delegateTestEnvToProvider"
|
|
350
|
-
:disabled="savingDelegation || !envRegistered"
|
|
351
|
-
@update:model-value="(v) => setDelegation({ delegateTestEnvToProvider: v })"
|
|
352
|
-
/>
|
|
353
|
-
<span class="text-sm text-slate-200">
|
|
354
|
-
{{ t('settings.providerConnection.delegation.envToggle') }}
|
|
355
|
-
</span>
|
|
356
|
-
</label>
|
|
357
|
-
<p class="pl-9 text-[11px] text-slate-400">
|
|
358
|
-
{{ t('settings.providerConnection.delegation.envHint') }}
|
|
359
|
-
</p>
|
|
360
|
-
</div>
|
|
361
|
-
</section>
|
|
362
|
-
|
|
363
|
-
<!-- In local mode the local-vs-external toggle for agents lives on the Ephemeral
|
|
364
|
-
environments screen (alongside the env toggle), so they're configured together. -->
|
|
365
|
-
<p
|
|
366
|
-
v-if="isLocal && kind === 'runner-pool'"
|
|
367
|
-
class="mb-4 rounded-md border border-slate-700 bg-slate-900/40 px-3 py-2 text-[11px] text-slate-400"
|
|
368
|
-
>
|
|
369
|
-
<i18n-t keypath="settings.providerConnection.runnerPoolLocalHint" tag="span" scope="global">
|
|
370
|
-
<template #link>
|
|
371
|
-
<button
|
|
372
|
-
type="button"
|
|
373
|
-
class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
|
|
374
|
-
@click="ui.openProviderConnection('environment')"
|
|
375
|
-
>
|
|
376
|
-
{{ t('settings.providerConnection.ephemeralEnvironments') }}
|
|
377
|
-
</button>
|
|
378
|
-
</template>
|
|
379
|
-
</i18n-t>
|
|
380
|
-
</p>
|
|
381
|
-
|
|
382
|
-
<div v-if="descriptor" class="space-y-4">
|
|
383
|
-
<div class="flex items-start justify-between gap-3">
|
|
384
|
-
<p class="text-xs text-slate-400">{{ meta?.blurb }}</p>
|
|
385
|
-
<UButton
|
|
386
|
-
:icon="showLogs ? 'i-lucide-chevron-up' : 'i-lucide-scroll-text'"
|
|
387
|
-
variant="ghost"
|
|
388
|
-
size="xs"
|
|
389
|
-
class="shrink-0"
|
|
390
|
-
@click="showLogs = !showLogs"
|
|
391
|
-
>
|
|
392
|
-
{{
|
|
393
|
-
showLogs
|
|
394
|
-
? t('settings.providerConnection.hideLogs')
|
|
395
|
-
: t('settings.providerConnection.viewLogs')
|
|
396
|
-
}}
|
|
397
|
-
</UButton>
|
|
398
|
-
</div>
|
|
399
|
-
|
|
400
|
-
<!-- Provisioning attempt history for this provider's subsystem. -->
|
|
401
|
-
<ProvisioningLogsDrawer v-if="showLogs && kind" :subsystem="kind" />
|
|
402
|
-
|
|
403
|
-
<!-- Saved connection summary -->
|
|
404
|
-
<div
|
|
405
|
-
v-if="connection"
|
|
406
|
-
class="flex items-center justify-between rounded-md border border-slate-700 bg-slate-900/50 px-3 py-2 text-sm"
|
|
407
|
-
>
|
|
408
|
-
<div>
|
|
409
|
-
<span class="font-medium text-slate-200">{{ connection.label }}</span>
|
|
410
|
-
<div class="text-[11px] text-emerald-400">
|
|
411
|
-
{{ t('settings.providerConnection.connectedAt', { baseUrl: connection.baseUrl }) }}
|
|
412
|
-
</div>
|
|
413
|
-
</div>
|
|
414
|
-
<UButton
|
|
415
|
-
icon="i-lucide-trash-2"
|
|
416
|
-
color="error"
|
|
417
|
-
variant="ghost"
|
|
418
|
-
size="xs"
|
|
419
|
-
:disabled="busy"
|
|
420
|
-
@click="remove()"
|
|
421
|
-
/>
|
|
422
|
-
</div>
|
|
423
|
-
|
|
424
|
-
<!-- Mandatory-fields warning (mirrors the banner) -->
|
|
425
|
-
<div
|
|
426
|
-
v-if="descriptor.missingRequired.length"
|
|
427
|
-
class="rounded-md border border-amber-500/40 bg-amber-950/40 px-3 py-2 text-xs text-amber-200"
|
|
428
|
-
>
|
|
429
|
-
{{
|
|
430
|
-
t('settings.providerConnection.missingConfig', {
|
|
431
|
-
fields: descriptor.missingRequired.join(', '),
|
|
432
|
-
})
|
|
433
|
-
}}
|
|
434
|
-
</div>
|
|
435
|
-
|
|
436
|
-
<!-- Generic, descriptor-driven field form -->
|
|
437
|
-
<div
|
|
438
|
-
v-if="canAuthor || canRotateSecrets"
|
|
439
|
-
class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3"
|
|
440
|
-
>
|
|
441
|
-
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
442
|
-
{{
|
|
443
|
-
connection
|
|
444
|
-
? t('settings.providerConnection.form.updateConfiguration')
|
|
445
|
-
: t('settings.providerConnection.form.connect')
|
|
446
|
-
}}
|
|
447
|
-
</p>
|
|
448
|
-
<!-- A native re-register replaces the whole manifest; secrets are write-only so they
|
|
449
|
-
must be re-supplied. Non-secret config is prefilled, so it survives a save. -->
|
|
450
|
-
<p
|
|
451
|
-
v-if="connection && canAuthor && hasSecretFields"
|
|
452
|
-
class="text-[11px] text-amber-300/80"
|
|
453
|
-
>
|
|
454
|
-
{{
|
|
455
|
-
t(
|
|
456
|
-
'settings.providerConnection.form.reenterSecrets',
|
|
457
|
-
{ count: secretFieldCount },
|
|
458
|
-
secretFieldCount,
|
|
459
|
-
)
|
|
460
|
-
}}
|
|
461
|
-
</p>
|
|
462
|
-
|
|
463
|
-
<UFormField
|
|
464
|
-
v-for="field in descriptor.configFields"
|
|
465
|
-
:key="field.key"
|
|
466
|
-
:label="
|
|
467
|
-
field.required && field.default === undefined
|
|
468
|
-
? field.label
|
|
469
|
-
: t('settings.providerConnection.form.optionalLabel', { label: field.label })
|
|
470
|
-
"
|
|
471
|
-
:help="fieldHelp(field.key)"
|
|
472
|
-
>
|
|
473
|
-
<USelect
|
|
474
|
-
v-if="field.type === 'select'"
|
|
475
|
-
v-model="values[field.key]"
|
|
476
|
-
:items="(field.options ?? []).map((o) => ({ label: o.label, value: o.value }))"
|
|
477
|
-
:placeholder="field.default ?? field.placeholder"
|
|
478
|
-
/>
|
|
479
|
-
<UInput
|
|
480
|
-
v-else
|
|
481
|
-
v-model="values[field.key]"
|
|
482
|
-
:type="field.secret ? 'password' : 'text'"
|
|
483
|
-
class="font-mono"
|
|
484
|
-
:placeholder="field.default ?? field.placeholder"
|
|
485
|
-
/>
|
|
486
|
-
</UFormField>
|
|
487
|
-
|
|
488
|
-
<div v-if="descriptor.supportsTest" class="flex items-center gap-2">
|
|
489
|
-
<UButton
|
|
490
|
-
color="neutral"
|
|
491
|
-
variant="soft"
|
|
492
|
-
size="sm"
|
|
493
|
-
icon="i-lucide-plug-zap"
|
|
494
|
-
:loading="testing"
|
|
495
|
-
@click="test()"
|
|
496
|
-
>
|
|
497
|
-
{{ t('settings.providerConnection.test.button') }}
|
|
498
|
-
</UButton>
|
|
499
|
-
<span v-if="testResult && testResult.ok" class="text-xs text-emerald-400">
|
|
500
|
-
{{ testResult.message ?? t('settings.providerConnection.test.ok') }}
|
|
501
|
-
</span>
|
|
502
|
-
<span v-else-if="testResult" class="text-xs text-rose-400">
|
|
503
|
-
{{ testResult.message ?? t('settings.providerConnection.test.failed') }}
|
|
504
|
-
</span>
|
|
505
|
-
</div>
|
|
506
|
-
|
|
507
|
-
<div class="flex justify-end">
|
|
508
|
-
<UButton color="primary" size="sm" :loading="busy" :disabled="!canSave" @click="save()">
|
|
509
|
-
{{ connection ? t('common.save') : t('settings.providerConnection.form.connect') }}
|
|
510
|
-
</UButton>
|
|
511
|
-
</div>
|
|
512
|
-
</div>
|
|
513
|
-
|
|
514
|
-
<!-- Manifest provider with nothing to overlay onto: needs the manifest editor -->
|
|
515
|
-
<div
|
|
516
|
-
v-else
|
|
517
|
-
class="rounded-md border border-slate-700 bg-slate-900/40 px-3 py-3 text-xs text-slate-400"
|
|
518
|
-
>
|
|
519
|
-
{{ t('settings.providerConnection.manifestEditorUnavailable') }}
|
|
520
|
-
</div>
|
|
521
|
-
</div>
|
|
522
|
-
</template>
|
|
523
|
-
</UModal>
|
|
524
|
-
</template>
|