@cat-factory/app 0.47.2 → 0.47.3
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/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
package/i18n/locales/uk.json
CHANGED
|
@@ -1137,13 +1137,11 @@
|
|
|
1137
1137
|
"label": "Стан після випуску",
|
|
1138
1138
|
"description": "Стежте за моніторами й SLO після випуску версії (Datadog)."
|
|
1139
1139
|
},
|
|
1140
|
-
"
|
|
1141
|
-
"label": "
|
|
1142
|
-
"description": "
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
"label": "Самостійно розміщений пул виконавців",
|
|
1146
|
-
"description": "Де виконуються агенти коду, коли не використовуються Cloudflare Containers."
|
|
1140
|
+
"infrastructure": {
|
|
1141
|
+
"label": "Інфраструктура",
|
|
1142
|
+
"description": "Самостійно розміщений пул виконавців для контейнерних агентів і ефемерні тестові середовища.",
|
|
1143
|
+
"agents": "Агенти: {state}",
|
|
1144
|
+
"envs": "Середовища: {state}"
|
|
1147
1145
|
},
|
|
1148
1146
|
"localMode": {
|
|
1149
1147
|
"label": "Локальний режим",
|
|
@@ -1212,6 +1210,12 @@
|
|
|
1212
1210
|
},
|
|
1213
1211
|
"providerConnection": {
|
|
1214
1212
|
"fallbackTitle": "Постачальник",
|
|
1213
|
+
"windowTitle": "Інфраструктура",
|
|
1214
|
+
"noneAvailable": "У цьому розгортанні не ввімкнено жодного постачальника інфраструктури.",
|
|
1215
|
+
"tabs": {
|
|
1216
|
+
"containerAgents": "Контейнерні агенти",
|
|
1217
|
+
"testEnvironments": "Тестові середовища"
|
|
1218
|
+
},
|
|
1215
1219
|
"kind": {
|
|
1216
1220
|
"environment": {
|
|
1217
1221
|
"title": "Постачальник ефемерних середовищ",
|
|
@@ -1222,6 +1226,18 @@
|
|
|
1222
1226
|
"blurb": "Місце, де працюють агенти кодування, коли не використовуються контейнери Cloudflare. Налаштуйте кінцеву точку планувальника пулу та облікові дані."
|
|
1223
1227
|
}
|
|
1224
1228
|
},
|
|
1229
|
+
"manifestEditor": {
|
|
1230
|
+
"title": "Маніфест постачальника",
|
|
1231
|
+
"jsonLabel": "Маніфест (JSON)",
|
|
1232
|
+
"jsonHelp": "Опишіть API свого постачальника декларативно: базову URL-адресу, схему автентифікації, шаблони запитів і відображення відповіді. Перевіряється за тим самим контрактом, який застосовує сервер.",
|
|
1233
|
+
"invalidJson": "Маніфест не є коректним JSON.",
|
|
1234
|
+
"invalidShape": "Маніфест не відповідає очікуваній структурі.",
|
|
1235
|
+
"schemaError": "Проблема з маніфестом — {message}",
|
|
1236
|
+
"secretsLabel": "Секрети",
|
|
1237
|
+
"noSecrets": "Цей маніфест не посилається на жодні секрети.",
|
|
1238
|
+
"reenterSecrets": "Введіть кожен секрет повторно, щоб зберегти — збережені секрети доступні лише для запису й не показуються.",
|
|
1239
|
+
"starterHint": "Це початковий приклад. Відредагуйте його відповідно до API вашого постачальника."
|
|
1240
|
+
},
|
|
1225
1241
|
"delegation": {
|
|
1226
1242
|
"title": "Локальне делегування",
|
|
1227
1243
|
"intro": "Типово ця машина виконує все локально — контейнерні агенти на Docker хоста, а інфраструктуру Tester через docker-compose всередині контейнера. Увімкніть опції нижче, щоб делегувати будь-яке з цих завдань зовнішньому сервісу. Застосовується лише в локальному режимі.",
|
|
@@ -1233,13 +1249,10 @@
|
|
|
1233
1249
|
"envHint": "Підніміть середовище попереднього перегляду Tester через постачальника середовищ, налаштованого нижче, замість docker-compose всередині контейнера. Спершу підключіть постачальника, щоб увімкнути це.",
|
|
1234
1250
|
"updateFailed": "Не вдалося оновити делегування"
|
|
1235
1251
|
},
|
|
1236
|
-
"runnerPoolLocalHint": "Зареєструйте свій пул тут, а потім увімкніть «Запускати контейнерні агенти в пулі виконавців» на екрані {link}, щоб спрямувати до нього агентів цього робочого простору.",
|
|
1237
|
-
"ephemeralEnvironments": "Ефемерні середовища",
|
|
1238
1252
|
"viewLogs": "Переглянути журнали",
|
|
1239
1253
|
"hideLogs": "Сховати журнали",
|
|
1240
1254
|
"connectedAt": "Підключено · {baseUrl}",
|
|
1241
1255
|
"missingConfig": "Відсутня обов'язкова конфігурація: {fields}",
|
|
1242
|
-
"manifestEditorUnavailable": "Цього постачальника налаштовують, складаючи маніфест. Вбудований редактор маніфестів ще недоступний — наразі зареєструйте його через API.",
|
|
1243
1256
|
"form": {
|
|
1244
1257
|
"updateConfiguration": "Оновити конфігурацію",
|
|
1245
1258
|
"connect": "Підключити",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.3",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"markdown-it": "^14.2.0",
|
|
32
32
|
"pinia": "^3.0.4",
|
|
33
33
|
"pinia-plugin-persistedstate": "^4.7.1",
|
|
34
|
+
"valibot": "^1.4.2",
|
|
34
35
|
"vue": "^3.5.39",
|
|
35
36
|
"wretch": "^3.0.9",
|
|
36
37
|
"@cat-factory/contracts": "0.44.0"
|
|
@@ -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>
|