@cat-factory/app 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/settings/InfrastructureBackendPicker.vue +304 -0
- package/app/components/settings/InfrastructureWindow.vue +6 -10
- package/app/components/settings/KubernetesRunnerForm.vue +30 -0
- package/app/components/settings/ProviderConnectionTab.vue +51 -67
- package/app/components/settings/ProviderManifestEditor.vue +19 -3
- package/app/composables/api/providerConnections.ts +18 -5
- package/app/stores/providerConnections.ts +59 -3
- package/app/stores/workspace.ts +8 -0
- package/app/types/providerConnections.ts +8 -1
- package/i18n/locales/en.json +16 -13
- package/i18n/locales/es.json +3 -11
- package/i18n/locales/fr.json +3 -11
- package/i18n/locales/pl.json +3 -11
- package/i18n/locales/uk.json +3 -11
- package/package.json +2 -2
- package/app/components/settings/ExecutionBackendSelector.vue +0 -171
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// ONE flat radio list per infrastructure axis — the single picker that replaces the old
|
|
3
|
+
// two-step "where it runs" radio + "runner/environment backend" dropdown. Each item is a
|
|
4
|
+
// concrete destination with a one-line description, so there is no more hidden second list:
|
|
5
|
+
// - execution axis → where agent containers run: the facade built-in (local Docker /
|
|
6
|
+
// Cloudflare Containers), the registered backend kinds (Kubernetes, a custom HTTP runner
|
|
7
|
+
// pool, plus any deployment-registered CUSTOM kind), and — in local mode — a low-config
|
|
8
|
+
// "Local Kubernetes (k3s)" preset.
|
|
9
|
+
// - testEnv axis → where the Tester's ephemeral environments run: the built-in
|
|
10
|
+
// (in-container docker-compose) + the registered backend kinds.
|
|
11
|
+
//
|
|
12
|
+
// The backend kinds come from the workspace snapshot (`providerConnections.backendKindsFor`),
|
|
13
|
+
// so a custom kind a deployment registered shows up as a first-class radio item. Selecting a
|
|
14
|
+
// pool/cluster item reveals its connect form inline (driven through ProviderConnectionTab). In
|
|
15
|
+
// LOCAL MODE the choice is a real per-workspace toggle (the `delegate*` settings) — but we
|
|
16
|
+
// DEFER writing `delegate=true` until a connection is actually registered, so a half-configured
|
|
17
|
+
// pick never routes runs to a non-existent pool. Off-local (Worker/Node) the active backend is
|
|
18
|
+
// deployment/registration-driven, so the radio doesn't write the toggle — it only reveals the
|
|
19
|
+
// connect forms — and a read-only "Active: …" line states what's effectively routing.
|
|
20
|
+
import { computed, ref, watch } from 'vue'
|
|
21
|
+
import type { ExecutionBackendKind, TestEnvBackendKind } from '@cat-factory/contracts'
|
|
22
|
+
import ProviderConnectionTab from '~/components/settings/ProviderConnectionTab.vue'
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{ axis: 'execution' | 'testEnv' }>()
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n()
|
|
27
|
+
const auth = useAuthStore()
|
|
28
|
+
const settings = useWorkspaceSettingsStore()
|
|
29
|
+
const providerConnections = useProviderConnectionsStore()
|
|
30
|
+
const toast = useToast()
|
|
31
|
+
|
|
32
|
+
type BackendKind = ExecutionBackendKind | TestEnvBackendKind
|
|
33
|
+
// A radio item: the built-in facade runtime, one per registered backend kind (built-in +
|
|
34
|
+
// custom), or the synthetic local-k3s preset. `backendKind` is the slug passed to the connect
|
|
35
|
+
// tab (absent on the built-in, which means "don't delegate"); `preset` prefills the k8s form.
|
|
36
|
+
interface PickerItem {
|
|
37
|
+
id: string
|
|
38
|
+
backendKind?: string
|
|
39
|
+
preset?: 'k3s'
|
|
40
|
+
label: string
|
|
41
|
+
desc: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The built-in facade runtimes carry localized labels + descriptions. i18n leaf keys are
|
|
45
|
+
// spelled as literals so the typed-message-keys check stays live.
|
|
46
|
+
const BUILTIN_KEYS: Record<BackendKind, { label: string; desc: string }> = {
|
|
47
|
+
'local-docker': {
|
|
48
|
+
label: 'settings.infrastructure.executionBackend.local-docker',
|
|
49
|
+
desc: 'settings.infrastructure.executionBackend.local-dockerDesc',
|
|
50
|
+
},
|
|
51
|
+
'cloudflare-containers': {
|
|
52
|
+
label: 'settings.infrastructure.executionBackend.cloudflare-containers',
|
|
53
|
+
desc: 'settings.infrastructure.executionBackend.cloudflare-containersDesc',
|
|
54
|
+
},
|
|
55
|
+
'runner-pool': {
|
|
56
|
+
label: 'settings.infrastructure.executionBackend.runner-pool',
|
|
57
|
+
desc: 'settings.infrastructure.executionBackend.runner-poolDesc',
|
|
58
|
+
},
|
|
59
|
+
'local-compose': {
|
|
60
|
+
label: 'settings.infrastructure.testEnvBackend.local-compose',
|
|
61
|
+
desc: 'settings.infrastructure.testEnvBackend.local-composeDesc',
|
|
62
|
+
},
|
|
63
|
+
'environment-provider': {
|
|
64
|
+
label: 'settings.infrastructure.testEnvBackend.environment-provider',
|
|
65
|
+
desc: 'settings.infrastructure.testEnvBackend.environment-providerDesc',
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
// The built-in `kubernetes` backend kind's localized label/desc, per axis.
|
|
69
|
+
const KUBERNETES_KEYS: Record<'execution' | 'testEnv', { label: string; desc: string }> = {
|
|
70
|
+
execution: {
|
|
71
|
+
label: 'settings.infrastructure.executionBackend.kubernetes',
|
|
72
|
+
desc: 'settings.infrastructure.executionBackend.kubernetesDesc',
|
|
73
|
+
},
|
|
74
|
+
testEnv: {
|
|
75
|
+
label: 'settings.infrastructure.testEnvBackend.kubernetes',
|
|
76
|
+
desc: 'settings.infrastructure.testEnvBackend.kubernetesDesc',
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
// k3s is an execution-only preset (the env k8s config can't be reduced to low-config), so a
|
|
80
|
+
// single catalog key serves it — no dead test-env keys.
|
|
81
|
+
const K3S_KEYS = {
|
|
82
|
+
label: 'settings.infrastructure.executionBackend.k3s',
|
|
83
|
+
desc: 'settings.infrastructure.executionBackend.k3sDesc',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const cap = computed<{ available: BackendKind[]; active: BackendKind } | null>(() => {
|
|
87
|
+
const c = auth.infrastructure?.[props.axis]
|
|
88
|
+
return c ? { available: c.available as BackendKind[], active: c.active as BackendKind } : null
|
|
89
|
+
})
|
|
90
|
+
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
91
|
+
const suggestedImage = computed(() => auth.infrastructure?.execution.suggestedExecutorImage)
|
|
92
|
+
|
|
93
|
+
// The kind reached by "delegating" away from the on-machine default + its connection kind.
|
|
94
|
+
const delegatedKind = computed<BackendKind>(() =>
|
|
95
|
+
props.axis === 'execution' ? 'runner-pool' : 'environment-provider',
|
|
96
|
+
)
|
|
97
|
+
const connectionKind = computed<'runner-pool' | 'environment'>(() =>
|
|
98
|
+
props.axis === 'execution' ? 'runner-pool' : 'environment',
|
|
99
|
+
)
|
|
100
|
+
// The built-in (the available option that isn't the delegated one), if this facade has one.
|
|
101
|
+
const builtinKind = computed<BackendKind | null>(
|
|
102
|
+
() => cap.value?.available.find((k) => k !== delegatedKind.value) ?? null,
|
|
103
|
+
)
|
|
104
|
+
const connection = computed(() => providerConnections.connectionFor(connectionKind.value))
|
|
105
|
+
const connectionRegistered = computed(() => !!connection.value)
|
|
106
|
+
// Pool/cluster items can be configured only when the deployment supports delegation AND the
|
|
107
|
+
// connect integration is enabled (not 503).
|
|
108
|
+
const poolConfigurable = computed(
|
|
109
|
+
() =>
|
|
110
|
+
(cap.value?.available.includes(delegatedKind.value) ?? false) &&
|
|
111
|
+
providerConnections.isAvailable(connectionKind.value),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// The delegation flag is a genuine per-workspace toggle ONLY in local mode.
|
|
115
|
+
const writable = computed(() => isLocal.value && (cap.value?.available.length ?? 0) > 1)
|
|
116
|
+
const delegated = computed(() =>
|
|
117
|
+
props.axis === 'execution'
|
|
118
|
+
? settings.settings.delegateAgentsToRunnerPool
|
|
119
|
+
: settings.settings.delegateTestEnvToProvider,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// The effective active backend (matches the prior ExecutionBackendSelector logic): local
|
|
123
|
+
// mode follows the toggle; off-local the delegated backend is active when its pool is
|
|
124
|
+
// registered, else the deployment default.
|
|
125
|
+
const effectiveActive = computed<BackendKind | null>(() => {
|
|
126
|
+
if (!cap.value) return builtinKind.value
|
|
127
|
+
if (writable.value) return delegated.value ? delegatedKind.value : builtinKind.value
|
|
128
|
+
if (cap.value.available.includes(delegatedKind.value) && connectionRegistered.value) {
|
|
129
|
+
return delegatedKind.value
|
|
130
|
+
}
|
|
131
|
+
return cap.value.active
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Localized label/desc for a built-in backend kind; null for a CUSTOM kind (which uses its
|
|
135
|
+
// snapshot label and has no description).
|
|
136
|
+
function backendKindKeys(kind: string): { label: string; desc: string } | null {
|
|
137
|
+
if (kind === 'kubernetes') return KUBERNETES_KEYS[props.axis]
|
|
138
|
+
if (kind === 'manifest') return BUILTIN_KEYS[delegatedKind.value]
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The radio item the current state maps to (an item id). A stored connection reads back as its
|
|
143
|
+
// own backend-kind slug; k3s is a one-shot prefill, never re-derived.
|
|
144
|
+
const derivedItem = computed<string>(() => {
|
|
145
|
+
if (effectiveActive.value === delegatedKind.value) return connection.value?.kind ?? 'kubernetes'
|
|
146
|
+
return 'builtin'
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const selected = ref<string>(derivedItem.value)
|
|
150
|
+
// Re-sync when the derived state changes (e.g. after a save/remove or a settings flip
|
|
151
|
+
// elsewhere). A pending pool/k3s pick before save doesn't move `derivedItem`, so the user's
|
|
152
|
+
// selection survives until a connection is registered.
|
|
153
|
+
watch(derivedItem, (v) => {
|
|
154
|
+
selected.value = v
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const items = computed<PickerItem[]>(() => {
|
|
158
|
+
const out: PickerItem[] = []
|
|
159
|
+
if (builtinKind.value) {
|
|
160
|
+
const k = BUILTIN_KEYS[builtinKind.value]
|
|
161
|
+
out.push({ id: 'builtin', label: t(k.label), desc: t(k.desc) })
|
|
162
|
+
}
|
|
163
|
+
if (poolConfigurable.value) {
|
|
164
|
+
for (const opt of providerConnections.backendKindsFor(connectionKind.value)) {
|
|
165
|
+
const keys = backendKindKeys(opt.kind)
|
|
166
|
+
out.push({
|
|
167
|
+
id: opt.kind,
|
|
168
|
+
backendKind: opt.kind,
|
|
169
|
+
label: keys ? t(keys.label) : opt.label,
|
|
170
|
+
desc: keys ? t(keys.desc) : '',
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
// The k3s low-config preset prefills the RUNNER k8s form; the env k8s config (manifest
|
|
174
|
+
// source + URL derivation) can't be reduced to low-config, so it's execution-only.
|
|
175
|
+
if (isLocal.value && props.axis === 'execution') {
|
|
176
|
+
out.push({
|
|
177
|
+
id: 'k3s',
|
|
178
|
+
backendKind: 'kubernetes',
|
|
179
|
+
preset: 'k3s',
|
|
180
|
+
label: t(K3S_KEYS.label),
|
|
181
|
+
desc: t(K3S_KEYS.desc),
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// The active-line label: prefer the registered connection's concrete kind over the generic
|
|
189
|
+
// "delegated" label so "Active: Kubernetes cluster" reads truthfully.
|
|
190
|
+
const activeLabel = computed(() => {
|
|
191
|
+
const c = connection.value
|
|
192
|
+
if (effectiveActive.value === delegatedKind.value && c?.kind) {
|
|
193
|
+
const keys = backendKindKeys(c.kind)
|
|
194
|
+
if (keys) return t(keys.label)
|
|
195
|
+
const opt = providerConnections
|
|
196
|
+
.backendKindsFor(connectionKind.value)
|
|
197
|
+
.find((o) => o.kind === c.kind)
|
|
198
|
+
return opt?.label ?? c.kind
|
|
199
|
+
}
|
|
200
|
+
const kind = effectiveActive.value
|
|
201
|
+
return kind ? t(BUILTIN_KEYS[kind].label) : ''
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const saving = ref(false)
|
|
205
|
+
|
|
206
|
+
async function setDelegate(value: boolean) {
|
|
207
|
+
saving.value = true
|
|
208
|
+
try {
|
|
209
|
+
await settings.update(
|
|
210
|
+
props.axis === 'execution'
|
|
211
|
+
? { delegateAgentsToRunnerPool: value }
|
|
212
|
+
: { delegateTestEnvToProvider: value },
|
|
213
|
+
)
|
|
214
|
+
} catch (e) {
|
|
215
|
+
toast.add({
|
|
216
|
+
title: t('settings.infrastructure.updateFailed'),
|
|
217
|
+
description: e instanceof Error ? e.message : String(e),
|
|
218
|
+
icon: 'i-lucide-triangle-alert',
|
|
219
|
+
color: 'error',
|
|
220
|
+
})
|
|
221
|
+
} finally {
|
|
222
|
+
saving.value = false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function select(id: string) {
|
|
227
|
+
selected.value = id
|
|
228
|
+
if (!writable.value) return // off-local: reveal the form, but don't flip a toggle.
|
|
229
|
+
if (id === 'builtin') {
|
|
230
|
+
await setDelegate(false)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
// Pool/cluster pick: only commit delegation now if a pool is already registered; otherwise
|
|
234
|
+
// defer to onConnected so we never route to a non-existent pool (the amber hint nags).
|
|
235
|
+
if (connectionRegistered.value) await setDelegate(true)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Emitted by ProviderConnectionTab after a connection is successfully registered: now it's
|
|
239
|
+
// safe to activate delegation for the pending pick.
|
|
240
|
+
async function onConnected() {
|
|
241
|
+
if (writable.value && selected.value !== 'builtin' && !delegated.value) await setDelegate(true)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Which connect form ProviderConnectionTab should show, and the k3s prefill signal.
|
|
245
|
+
const selectedItem = computed<PickerItem | undefined>(() =>
|
|
246
|
+
items.value.find((i) => i.id === selected.value),
|
|
247
|
+
)
|
|
248
|
+
const showConnectForm = computed(() => !!selectedItem.value?.backendKind && poolConfigurable.value)
|
|
249
|
+
const selectedBackendKind = computed(() => selectedItem.value?.backendKind ?? 'manifest')
|
|
250
|
+
const selectedPreset = computed(() => selectedItem.value?.preset)
|
|
251
|
+
// Nag when a pool item is picked in local mode but no pool is registered to back it.
|
|
252
|
+
const showRegisterHint = computed(
|
|
253
|
+
() => writable.value && selected.value !== 'builtin' && !connectionRegistered.value,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const labelKey = computed(() =>
|
|
257
|
+
props.axis === 'execution'
|
|
258
|
+
? 'settings.infrastructure.executionBackend.label'
|
|
259
|
+
: 'settings.infrastructure.testEnvBackend.label',
|
|
260
|
+
)
|
|
261
|
+
</script>
|
|
262
|
+
|
|
263
|
+
<template>
|
|
264
|
+
<section v-if="cap" class="space-y-2 rounded-lg border border-slate-700 bg-slate-900/40 p-3">
|
|
265
|
+
<h3 class="text-sm font-semibold text-slate-200">{{ t(labelKey) }}</h3>
|
|
266
|
+
|
|
267
|
+
<!-- Off-local: the active backend is deployment/registration-driven; state it plainly. -->
|
|
268
|
+
<p v-if="!writable" class="text-sm text-slate-300" :data-testid="`${axis}-backend-active`">
|
|
269
|
+
{{ t('settings.infrastructure.active', { backend: activeLabel }) }}
|
|
270
|
+
</p>
|
|
271
|
+
|
|
272
|
+
<div class="space-y-1.5" :data-testid="`${axis}-backend-options`">
|
|
273
|
+
<label v-for="item in items" :key="item.id" class="flex cursor-pointer items-start gap-2">
|
|
274
|
+
<input
|
|
275
|
+
type="radio"
|
|
276
|
+
class="mt-1"
|
|
277
|
+
:value="item.id"
|
|
278
|
+
:checked="item.id === selected"
|
|
279
|
+
:disabled="saving"
|
|
280
|
+
:data-testid="`${axis}-backend-${item.id}`"
|
|
281
|
+
@change="select(item.id)"
|
|
282
|
+
/>
|
|
283
|
+
<span class="min-w-0">
|
|
284
|
+
<span class="text-sm text-slate-200">{{ item.label }}</span>
|
|
285
|
+
<span v-if="item.desc" class="block text-[11px] text-slate-400">{{ item.desc }}</span>
|
|
286
|
+
</span>
|
|
287
|
+
</label>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<p v-if="showRegisterHint" class="text-[11px] text-amber-300/80">
|
|
291
|
+
{{ t('settings.infrastructure.registerHint') }}
|
|
292
|
+
</p>
|
|
293
|
+
|
|
294
|
+
<!-- The connect form for the selected pool/cluster, driven through the shared tab. -->
|
|
295
|
+
<ProviderConnectionTab
|
|
296
|
+
v-if="showConnectForm"
|
|
297
|
+
:kind="connectionKind"
|
|
298
|
+
:backend-kind="selectedBackendKind"
|
|
299
|
+
:preset="selectedPreset"
|
|
300
|
+
:suggested-image="suggestedImage"
|
|
301
|
+
@connected="onConnected"
|
|
302
|
+
/>
|
|
303
|
+
</section>
|
|
304
|
+
</template>
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
// backend integration is disabled (503) simply doesn't render.
|
|
12
12
|
import { computed, ref, watch } from 'vue'
|
|
13
13
|
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
14
|
-
import
|
|
15
|
-
import ExecutionBackendSelector from '~/components/settings/ExecutionBackendSelector.vue'
|
|
14
|
+
import InfrastructureBackendPicker from '~/components/settings/InfrastructureBackendPicker.vue'
|
|
16
15
|
import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
|
|
17
16
|
|
|
18
17
|
const { t } = useI18n()
|
|
@@ -103,10 +102,9 @@ watch([tabs, () => store.loaded], () => {
|
|
|
103
102
|
>
|
|
104
103
|
<template #runner-pool>
|
|
105
104
|
<div class="space-y-4">
|
|
106
|
-
<!--
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<ProviderConnectionTab v-if="store.isAvailable('runner-pool')" kind="runner-pool" />
|
|
105
|
+
<!-- One unified list of where agent containers run; the selected pool/cluster
|
|
106
|
+
reveals its connect form inline. -->
|
|
107
|
+
<InfrastructureBackendPicker axis="execution" />
|
|
110
108
|
<!-- Local mode: the warm-pool + checkout reuse ARE the host agent-container
|
|
111
109
|
runtime, so they live here rather than in a separate menu. -->
|
|
112
110
|
<section v-if="isLocal" class="border-t border-slate-800 pt-4">
|
|
@@ -119,10 +117,8 @@ watch([tabs, () => store.loaded], () => {
|
|
|
119
117
|
</template>
|
|
120
118
|
<template #environment>
|
|
121
119
|
<div class="space-y-4">
|
|
122
|
-
<!--
|
|
123
|
-
<
|
|
124
|
-
<!-- The environment-provider connect form only when that integration is enabled. -->
|
|
125
|
-
<ProviderConnectionTab v-if="store.isAvailable('environment')" kind="environment" />
|
|
120
|
+
<!-- One unified list of where the Tester's ephemeral environments run. -->
|
|
121
|
+
<InfrastructureBackendPicker axis="testEnv" />
|
|
126
122
|
</div>
|
|
127
123
|
</template>
|
|
128
124
|
</UTabs>
|
|
@@ -9,6 +9,10 @@ import type { ProviderConnection } from '~/types/providerConnections'
|
|
|
9
9
|
|
|
10
10
|
const props = defineProps<{
|
|
11
11
|
connection: ProviderConnection | null
|
|
12
|
+
/** A low-config preset to seed the form with (today: local k3s). */
|
|
13
|
+
preset?: 'k3s'
|
|
14
|
+
/** The deployment's executor image, used to prefill the k3s preset's image field. */
|
|
15
|
+
suggestedImage?: string
|
|
12
16
|
supportsTest: boolean
|
|
13
17
|
testing: boolean
|
|
14
18
|
busy: boolean
|
|
@@ -30,6 +34,7 @@ const form = reactive({
|
|
|
30
34
|
imageUi: '',
|
|
31
35
|
caCertPem: '',
|
|
32
36
|
harnessPort: '',
|
|
37
|
+
insecureSkipTlsVerify: false,
|
|
33
38
|
})
|
|
34
39
|
const apiToken = ref('')
|
|
35
40
|
|
|
@@ -52,11 +57,28 @@ watch(
|
|
|
52
57
|
form.imageUi = typeof k.imageUi === 'string' ? k.imageUi : ''
|
|
53
58
|
form.caCertPem = typeof k.caCertPem === 'string' ? k.caCertPem : ''
|
|
54
59
|
form.harnessPort = typeof k.harnessPort === 'number' ? String(k.harnessPort) : ''
|
|
60
|
+
form.insecureSkipTlsVerify = k.insecureSkipTlsVerify === true
|
|
55
61
|
}
|
|
56
62
|
},
|
|
57
63
|
{ immediate: true },
|
|
58
64
|
)
|
|
59
65
|
|
|
66
|
+
// Low-config k3s preset: seed the local-cluster defaults so the operator only pastes a
|
|
67
|
+
// ServiceAccount token (and an image, unless the deployment surfaced one). Only seeds a
|
|
68
|
+
// fresh form — never clobbers an existing connection's config on edit.
|
|
69
|
+
watch(
|
|
70
|
+
() => props.preset,
|
|
71
|
+
(preset) => {
|
|
72
|
+
if (preset !== 'k3s' || props.connection) return
|
|
73
|
+
form.label = 'Local k3s'
|
|
74
|
+
form.apiServerUrl = 'https://127.0.0.1:6443'
|
|
75
|
+
form.namespace = 'cat-factory'
|
|
76
|
+
form.insecureSkipTlsVerify = true
|
|
77
|
+
if (props.suggestedImage) form.image = props.suggestedImage
|
|
78
|
+
},
|
|
79
|
+
{ immediate: true },
|
|
80
|
+
)
|
|
81
|
+
|
|
60
82
|
const canSave = computed(
|
|
61
83
|
() =>
|
|
62
84
|
!!form.label.trim() &&
|
|
@@ -75,6 +97,7 @@ function buildPayload(): { config: Record<string, unknown>; secrets: Record<stri
|
|
|
75
97
|
}
|
|
76
98
|
if (form.imageUi.trim()) kubernetes.imageUi = form.imageUi.trim()
|
|
77
99
|
if (form.caCertPem.trim()) kubernetes.caCertPem = form.caCertPem.trim()
|
|
100
|
+
if (form.insecureSkipTlsVerify) kubernetes.insecureSkipTlsVerify = true
|
|
78
101
|
const port = Number(form.harnessPort)
|
|
79
102
|
if (form.harnessPort.trim() && Number.isFinite(port)) kubernetes.harnessPort = port
|
|
80
103
|
return {
|
|
@@ -156,6 +179,13 @@ function buildPayload(): { config: Record<string, unknown>; secrets: Record<stri
|
|
|
156
179
|
/>
|
|
157
180
|
</UFormField>
|
|
158
181
|
|
|
182
|
+
<UFormField :help="t('settings.providerConnection.kubernetes.insecureSkipTlsVerifyHelp')">
|
|
183
|
+
<UCheckbox
|
|
184
|
+
v-model="form.insecureSkipTlsVerify"
|
|
185
|
+
:label="t('settings.providerConnection.kubernetes.insecureSkipTlsVerify')"
|
|
186
|
+
/>
|
|
187
|
+
</UFormField>
|
|
188
|
+
|
|
159
189
|
<UFormField
|
|
160
190
|
:label="
|
|
161
191
|
t('settings.providerConnection.form.optionalLabel', {
|
|
@@ -13,7 +13,18 @@ import ProviderManifestEditor from '~/components/settings/ProviderManifestEditor
|
|
|
13
13
|
import KubernetesRunnerForm from '~/components/settings/KubernetesRunnerForm.vue'
|
|
14
14
|
import KubernetesEnvironmentForm from '~/components/settings/KubernetesEnvironmentForm.vue'
|
|
15
15
|
|
|
16
|
-
const props = defineProps<{
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
kind: ProviderConnectionKind
|
|
18
|
+
/** The selected backend-kind slug — chosen by the parent picker's radio, not a local
|
|
19
|
+
* dropdown. Built-in (`manifest`/`kubernetes`) or a deployment-registered custom kind. */
|
|
20
|
+
backendKind: string
|
|
21
|
+
/** A low-config preset to prefill the Kubernetes form (today: local k3s). */
|
|
22
|
+
preset?: 'k3s'
|
|
23
|
+
/** The deployment's executor image, used to prefill the k3s preset's image field. */
|
|
24
|
+
suggestedImage?: string
|
|
25
|
+
}>()
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{ connected: [] }>()
|
|
17
28
|
|
|
18
29
|
const { t } = useI18n()
|
|
19
30
|
const store = useProviderConnectionsStore()
|
|
@@ -31,7 +42,7 @@ const title = computed(() => t(`settings.providerConnection.kind.${props.kind}.t
|
|
|
31
42
|
|
|
32
43
|
watch(
|
|
33
44
|
() => props.kind,
|
|
34
|
-
(k) => void store.loadKind(k).then(resetDraft),
|
|
45
|
+
(k) => void store.loadKind(k, props.backendKind).then(resetDraft),
|
|
35
46
|
{ immediate: true },
|
|
36
47
|
)
|
|
37
48
|
|
|
@@ -88,6 +99,7 @@ const canSave = computed(() => {
|
|
|
88
99
|
function buildManifestPayload(): {
|
|
89
100
|
manifest: Record<string, unknown>
|
|
90
101
|
secrets: Record<string, string>
|
|
102
|
+
backendKind: string
|
|
91
103
|
} | null {
|
|
92
104
|
const template = descriptor.value?.manifestTemplate
|
|
93
105
|
if (!template) return null
|
|
@@ -106,7 +118,9 @@ function buildManifestPayload(): {
|
|
|
106
118
|
else providerConfig[f.key] = val
|
|
107
119
|
}
|
|
108
120
|
if (Object.keys(providerConfig).length) manifest.providerConfig = providerConfig
|
|
109
|
-
|
|
121
|
+
// Carry the selected kind so a CUSTOM backend's flat-form save is tagged with its slug
|
|
122
|
+
// (not silently wrapped into the built-in `manifest` backend).
|
|
123
|
+
return { manifest, secrets, backendKind: props.backendKind }
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
function notifyError(title: string, e: unknown) {
|
|
@@ -146,6 +160,7 @@ async function saveNative() {
|
|
|
146
160
|
try {
|
|
147
161
|
const payload = buildManifestPayload()
|
|
148
162
|
if (payload) await store.register(props.kind, payload)
|
|
163
|
+
emit('connected')
|
|
149
164
|
resetDraft()
|
|
150
165
|
toastSaved()
|
|
151
166
|
} catch (e) {
|
|
@@ -156,6 +171,8 @@ async function saveNative() {
|
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
// --- Manifest-editor actions (emitted from ProviderManifestEditor) ------------------
|
|
174
|
+
// Tag the raw-manifest save/test with the selected backend kind too, so a CUSTOM kind that
|
|
175
|
+
// ships no flat-form template (and thus uses the raw editor) isn't mis-tagged as `manifest`.
|
|
159
176
|
async function testManifest(payload: {
|
|
160
177
|
manifest: Record<string, unknown>
|
|
161
178
|
secrets: Record<string, string>
|
|
@@ -163,7 +180,7 @@ async function testManifest(payload: {
|
|
|
163
180
|
testing.value = true
|
|
164
181
|
testResult.value = null
|
|
165
182
|
try {
|
|
166
|
-
testResult.value = await store.test(props.kind, payload)
|
|
183
|
+
testResult.value = await store.test(props.kind, { ...payload, backendKind: props.backendKind })
|
|
167
184
|
} catch (e) {
|
|
168
185
|
testResult.value = { ok: false, message: e instanceof Error ? e.message : String(e) }
|
|
169
186
|
} finally {
|
|
@@ -177,7 +194,8 @@ async function saveManifest(payload: {
|
|
|
177
194
|
}) {
|
|
178
195
|
busy.value = true
|
|
179
196
|
try {
|
|
180
|
-
await store.register(props.kind, payload)
|
|
197
|
+
await store.register(props.kind, { ...payload, backendKind: props.backendKind })
|
|
198
|
+
emit('connected')
|
|
181
199
|
toastSaved()
|
|
182
200
|
} catch (e) {
|
|
183
201
|
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
@@ -186,39 +204,18 @@ async function saveManifest(payload: {
|
|
|
186
204
|
}
|
|
187
205
|
}
|
|
188
206
|
|
|
189
|
-
// --- Backend
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const backendSelectorLabel = computed(() =>
|
|
199
|
-
t(
|
|
200
|
-
props.kind === 'environment'
|
|
201
|
-
? 'settings.providerConnection.backend.environmentSelectorLabel'
|
|
202
|
-
: 'settings.providerConnection.backend.selectorLabel',
|
|
203
|
-
),
|
|
204
|
-
)
|
|
205
|
-
function backendKindLabel(k: BackendKind): string {
|
|
206
|
-
if (k === 'kubernetes') return t('settings.providerConnection.backend.kubernetes')
|
|
207
|
-
return t(
|
|
208
|
-
props.kind === 'environment'
|
|
209
|
-
? 'settings.providerConnection.backend.environmentManifest'
|
|
210
|
-
: 'settings.providerConnection.backend.manifest',
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
const backendKindItems = computed(() =>
|
|
214
|
-
BACKEND_KINDS.map((k) => ({ label: backendKindLabel(k), value: k })),
|
|
215
|
-
)
|
|
207
|
+
// --- Backend connect forms ------------------------------------------------------------
|
|
208
|
+
// Which backend kind to configure (the built-in `manifest`/`kubernetes` backends or any
|
|
209
|
+
// CUSTOM kind a deployment registered) is decided by the parent picker's unified radio and
|
|
210
|
+
// passed in as `backendKind` — there is no longer a local dropdown here. The two K8s
|
|
211
|
+
// backends have bespoke forms; every other kind (manifest + custom) uses the descriptor-
|
|
212
|
+
// driven flat form / raw manifest editor. Switching the kind re-probes ONLY that kind's
|
|
213
|
+
// descriptor (so a not-yet-connected custom kind's connect form renders) WITHOUT re-fetching
|
|
214
|
+
// the stored connection — using `loadDescriptor` (not `loadKind`) avoids bouncing the
|
|
215
|
+
// picker's selection back to the stored kind via a connection re-read.
|
|
216
216
|
watch(
|
|
217
|
-
() =>
|
|
218
|
-
(
|
|
219
|
-
if (c?.kind === 'kubernetes' || c?.kind === 'manifest') backendKind.value = c.kind
|
|
220
|
-
},
|
|
221
|
-
{ immediate: true },
|
|
217
|
+
() => props.backendKind,
|
|
218
|
+
(k) => void store.loadDescriptor(props.kind, k).then(resetDraft),
|
|
222
219
|
)
|
|
223
220
|
|
|
224
221
|
async function testConfig(payload: {
|
|
@@ -243,6 +240,7 @@ async function saveConfig(payload: {
|
|
|
243
240
|
busy.value = true
|
|
244
241
|
try {
|
|
245
242
|
await store.register(props.kind, payload)
|
|
243
|
+
emit('connected')
|
|
246
244
|
toastSaved()
|
|
247
245
|
} catch (e) {
|
|
248
246
|
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
@@ -332,15 +330,12 @@ function fieldHelp(key: string): string | undefined {
|
|
|
332
330
|
}}
|
|
333
331
|
</div>
|
|
334
332
|
|
|
335
|
-
<!-- Backend selector: the BYO manifest backend or a native Kubernetes backend. -->
|
|
336
|
-
<UFormField v-if="showBackendSelector" :label="backendSelectorLabel">
|
|
337
|
-
<USelect v-model="backendKind" :items="backendKindItems" />
|
|
338
|
-
</UFormField>
|
|
339
|
-
|
|
340
333
|
<!-- Native Kubernetes runner backend (runner-pool). -->
|
|
341
334
|
<KubernetesRunnerForm
|
|
342
335
|
v-if="kind === 'runner-pool' && backendKind === 'kubernetes'"
|
|
343
336
|
:connection="connection"
|
|
337
|
+
:preset="preset"
|
|
338
|
+
:suggested-image="suggestedImage"
|
|
344
339
|
:supports-test="descriptor.supportsTest"
|
|
345
340
|
:testing="testing"
|
|
346
341
|
:busy="busy"
|
|
@@ -440,31 +435,20 @@ function fieldHelp(key: string): string | undefined {
|
|
|
440
435
|
</div>
|
|
441
436
|
</div>
|
|
442
437
|
|
|
443
|
-
<!-- MANIFEST-driven provider: the raw JSON manifest editor.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
<details
|
|
438
|
+
<!-- MANIFEST-driven provider: the raw JSON manifest editor. The radio already selected
|
|
439
|
+
"custom HTTP" so it's shown expanded — no extra disclosure. -->
|
|
440
|
+
<ProviderManifestEditor
|
|
447
441
|
v-else
|
|
448
|
-
|
|
449
|
-
:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
:saved-manifest="descriptor.savedManifest"
|
|
460
|
-
:connected="!!connection"
|
|
461
|
-
:supports-test="descriptor.supportsTest"
|
|
462
|
-
:testing="testing"
|
|
463
|
-
:busy="busy"
|
|
464
|
-
:test-result="testResult"
|
|
465
|
-
@test="testManifest"
|
|
466
|
-
@save="saveManifest"
|
|
467
|
-
/>
|
|
468
|
-
</details>
|
|
442
|
+
:kind="kind"
|
|
443
|
+
:saved-manifest="descriptor.savedManifest"
|
|
444
|
+
:connected="!!connection"
|
|
445
|
+
:stored-secret-keys="connection?.secretKeys ?? []"
|
|
446
|
+
:supports-test="descriptor.supportsTest"
|
|
447
|
+
:testing="testing"
|
|
448
|
+
:busy="busy"
|
|
449
|
+
:test-result="testResult"
|
|
450
|
+
@test="testManifest"
|
|
451
|
+
@save="saveManifest"
|
|
452
|
+
/>
|
|
469
453
|
</div>
|
|
470
454
|
</template>
|
|
@@ -25,6 +25,9 @@ const props = defineProps<{
|
|
|
25
25
|
savedManifest?: Record<string, unknown>
|
|
26
26
|
/** Whether a connection already exists (drives the re-enter-secrets hint + button label). */
|
|
27
27
|
connected: boolean
|
|
28
|
+
/** The secret keys already stored for this connection (names only) — shown next to the
|
|
29
|
+
* write-only inputs so it's obvious what exists without scrolling to the summary. */
|
|
30
|
+
storedSecretKeys?: string[]
|
|
28
31
|
/** Whether the provider exposes a connection test the UI can call. */
|
|
29
32
|
supportsTest: boolean
|
|
30
33
|
/** Bubbled-up busy state from the tab's store calls (so the editor shows loading). */
|
|
@@ -235,9 +238,22 @@ function onSave() {
|
|
|
235
238
|
<p v-if="!secretKeys.length" class="text-[11px] text-slate-500">
|
|
236
239
|
{{ t('settings.providerConnection.manifestEditor.noSecrets') }}
|
|
237
240
|
</p>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
<template v-else-if="connected">
|
|
242
|
+
<p
|
|
243
|
+
v-if="storedSecretKeys && storedSecretKeys.length"
|
|
244
|
+
class="text-[11px] text-slate-400"
|
|
245
|
+
data-testid="manifest-editor-stored"
|
|
246
|
+
>
|
|
247
|
+
{{
|
|
248
|
+
t('settings.providerConnection.manifestEditor.stored', {
|
|
249
|
+
keys: storedSecretKeys.join(', '),
|
|
250
|
+
})
|
|
251
|
+
}}
|
|
252
|
+
</p>
|
|
253
|
+
<p class="text-[11px] text-amber-300/80">
|
|
254
|
+
{{ t('settings.providerConnection.manifestEditor.reenterSecrets') }}
|
|
255
|
+
</p>
|
|
256
|
+
</template>
|
|
241
257
|
<UFormField v-for="key in secretKeys" :key="key" :label="key">
|
|
242
258
|
<UInput
|
|
243
259
|
v-model="secrets[key]"
|
|
@@ -51,8 +51,20 @@ const CONTRACTS = {
|
|
|
51
51
|
/** Environment-provider + runner-pool connection endpoints (self-describe + register/test). */
|
|
52
52
|
export function providerConnectionsApi({ send, ws }: ApiContext) {
|
|
53
53
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// `backendKind` (optional) describes a REGISTERED backend that isn't connected yet, so a
|
|
55
|
+
// custom kind's connect form renders before the first connect. Omitted ⇒ the stored kind.
|
|
56
|
+
// Branch on the kind so `send` sees a single concrete contract (a union contract can't
|
|
57
|
+
// type-check the optional `queryParams`).
|
|
58
|
+
describeProvider: (workspaceId: string, kind: ProviderConnectionKind, backendKind?: string) =>
|
|
59
|
+
kind === 'environment'
|
|
60
|
+
? send(CONTRACTS.environment.describe, {
|
|
61
|
+
pathPrefix: ws(workspaceId),
|
|
62
|
+
queryParams: { kind: backendKind },
|
|
63
|
+
})
|
|
64
|
+
: send(CONTRACTS['runner-pool'].describe, {
|
|
65
|
+
pathPrefix: ws(workspaceId),
|
|
66
|
+
queryParams: { kind: backendKind },
|
|
67
|
+
}),
|
|
56
68
|
|
|
57
69
|
getProviderConnection: (workspaceId: string, kind: ProviderConnectionKind) =>
|
|
58
70
|
send(CONTRACTS[kind].get, { pathPrefix: ws(workspaceId) }),
|
|
@@ -118,10 +130,11 @@ export function providerConnectionsApi({ send, ws }: ApiContext) {
|
|
|
118
130
|
/**
|
|
119
131
|
* Resolve the discriminated backend config (runner-pool OR environment) from a connect-form
|
|
120
132
|
* payload: an explicit `config` (the Kubernetes form) wins; otherwise a bare `manifest` (the
|
|
121
|
-
* manifest editor) is wrapped into the
|
|
122
|
-
*
|
|
133
|
+
* flat form / manifest editor) is wrapped into the selected backend kind — `body.backendKind`
|
|
134
|
+
* (a built-in `manifest` or a registered CUSTOM slug), defaulting to `manifest`. A custom kind
|
|
135
|
+
* MUST carry its slug here, else its flat-form save would be mis-tagged as the manifest backend.
|
|
123
136
|
*/
|
|
124
137
|
function backendConfig(body: RegisterProviderInput | TestProviderInput): Record<string, unknown> {
|
|
125
138
|
if (body.config) return body.config
|
|
126
|
-
return { kind: 'manifest', manifest: body.manifest ?? {} }
|
|
139
|
+
return { kind: body.backendKind ?? 'manifest', manifest: body.manifest ?? {} }
|
|
127
140
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { computed, reactive, ref } from 'vue'
|
|
3
|
+
import type { BackendKindOption } from '@cat-factory/contracts'
|
|
3
4
|
import type {
|
|
4
5
|
ProviderConnection,
|
|
5
6
|
ProviderConnectionKind,
|
|
@@ -11,6 +12,21 @@ import { useWorkspaceStore } from '~/stores/workspace'
|
|
|
11
12
|
|
|
12
13
|
const KINDS: ProviderConnectionKind[] = ['environment', 'runner-pool']
|
|
13
14
|
|
|
15
|
+
// Built-in fallback so the connect form's backend selector works before the snapshot
|
|
16
|
+
// loads (or on an older backend that doesn't advertise the kinds). The live lists come
|
|
17
|
+
// from the workspace snapshot (`environmentBackendKinds` / `runnerBackendKinds`) and may
|
|
18
|
+
// additionally carry a deployment's programmatically-registered CUSTOM kinds.
|
|
19
|
+
const BUILTIN_BACKEND_KINDS: Record<ProviderConnectionKind, BackendKindOption[]> = {
|
|
20
|
+
environment: [
|
|
21
|
+
{ kind: 'manifest', label: 'HTTP manifest' },
|
|
22
|
+
{ kind: 'kubernetes', label: 'Kubernetes' },
|
|
23
|
+
],
|
|
24
|
+
'runner-pool': [
|
|
25
|
+
{ kind: 'manifest', label: 'HTTP manifest pool' },
|
|
26
|
+
{ kind: 'kubernetes', label: 'Kubernetes' },
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
interface ProviderState {
|
|
15
31
|
/** null until first probed; false ⇒ integration disabled on the backend (hide it). */
|
|
16
32
|
available: boolean | null
|
|
@@ -38,13 +54,35 @@ export const useProviderConnectionsStore = defineStore('providerConnections', ()
|
|
|
38
54
|
})
|
|
39
55
|
const loaded = ref(false)
|
|
40
56
|
let inFlight: Promise<void> | null = null
|
|
57
|
+
// The selectable backend kinds per subsystem, fed from the workspace snapshot.
|
|
58
|
+
const backendKinds = reactive<Record<ProviderConnectionKind, BackendKindOption[]>>({
|
|
59
|
+
environment: BUILTIN_BACKEND_KINDS.environment,
|
|
60
|
+
'runner-pool': BUILTIN_BACKEND_KINDS['runner-pool'],
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** Seed the backend-kind selector lists from the workspace snapshot (built-in + custom). */
|
|
64
|
+
function registerBackendKinds(
|
|
65
|
+
payload: Partial<Record<ProviderConnectionKind, BackendKindOption[]>>,
|
|
66
|
+
) {
|
|
67
|
+
for (const kind of KINDS) {
|
|
68
|
+
const list = payload[kind]
|
|
69
|
+
if (list && list.length > 0) backendKinds[kind] = list
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The selectable backend kinds for a subsystem (built-in fallback until the snapshot loads). */
|
|
74
|
+
function backendKindsFor(kind: ProviderConnectionKind): BackendKindOption[] {
|
|
75
|
+
return backendKinds[kind]
|
|
76
|
+
}
|
|
41
77
|
|
|
42
|
-
|
|
78
|
+
// `backendKind` (optional) re-probes the descriptor for a specific (e.g. not-yet-connected
|
|
79
|
+
// custom) backend kind, so its connect form renders before the first connect.
|
|
80
|
+
async function loadKind(kind: ProviderConnectionKind, backendKind?: string) {
|
|
43
81
|
const ws = useWorkspaceStore()
|
|
44
82
|
const s = state[kind]
|
|
45
83
|
try {
|
|
46
84
|
const [descriptor, { connection }] = await Promise.all([
|
|
47
|
-
api.describeProvider(ws.requireId(), kind),
|
|
85
|
+
api.describeProvider(ws.requireId(), kind, backendKind),
|
|
48
86
|
api.getProviderConnection(ws.requireId(), kind),
|
|
49
87
|
])
|
|
50
88
|
s.descriptor = descriptor
|
|
@@ -58,9 +96,24 @@ export const useProviderConnectionsStore = defineStore('providerConnections', ()
|
|
|
58
96
|
}
|
|
59
97
|
}
|
|
60
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Re-probe ONLY the descriptor for a specific backend kind (e.g. a not-yet-connected
|
|
101
|
+
* custom kind the user just picked), leaving the stored connection untouched. Switching
|
|
102
|
+
* the selector must NOT re-fetch the connection: that would reassign `state.connection`
|
|
103
|
+
* and bounce the selector back to the stored kind via the component's `connection` watch.
|
|
104
|
+
*/
|
|
105
|
+
async function loadDescriptor(kind: ProviderConnectionKind, backendKind?: string) {
|
|
106
|
+
const ws = useWorkspaceStore()
|
|
107
|
+
try {
|
|
108
|
+
state[kind].descriptor = await api.describeProvider(ws.requireId(), kind, backendKind)
|
|
109
|
+
} catch {
|
|
110
|
+
// Keep the existing descriptor/availability on a transient describe failure.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
61
114
|
/** Refresh both providers (used by the banner + after a save/remove). */
|
|
62
115
|
async function load() {
|
|
63
|
-
await Promise.all(KINDS.map(loadKind))
|
|
116
|
+
await Promise.all(KINDS.map((k) => loadKind(k)))
|
|
64
117
|
loaded.value = true
|
|
65
118
|
}
|
|
66
119
|
|
|
@@ -123,11 +176,14 @@ export const useProviderConnectionsStore = defineStore('providerConnections', ()
|
|
|
123
176
|
loaded,
|
|
124
177
|
load,
|
|
125
178
|
loadKind,
|
|
179
|
+
loadDescriptor,
|
|
126
180
|
ensureLoaded,
|
|
127
181
|
descriptorFor,
|
|
128
182
|
connectionFor,
|
|
129
183
|
isAvailable,
|
|
130
184
|
needingConfig,
|
|
185
|
+
backendKindsFor,
|
|
186
|
+
registerBackendKinds,
|
|
131
187
|
register,
|
|
132
188
|
updateSecrets,
|
|
133
189
|
test,
|
package/app/stores/workspace.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { useClarityStore } from '~/stores/clarity'
|
|
|
21
21
|
import { useBrainstormStore } from '~/stores/brainstorm'
|
|
22
22
|
import { useConsensusStore } from '~/stores/consensus'
|
|
23
23
|
import { useGitHubStore } from '~/stores/github'
|
|
24
|
+
import { useProviderConnectionsStore } from '~/stores/providerConnections'
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Owns the active workspace and bootstraps the app against the backend. On load
|
|
@@ -98,6 +99,13 @@ export const useWorkspaceStore = defineStore(
|
|
|
98
99
|
// Merge the deployment's registered custom agent kinds into the palette catalog so a
|
|
99
100
|
// proprietary kind renders as a first-class block + result view (idempotent on reload).
|
|
100
101
|
useAgentsStore().registerCustomKinds(snapshot.customAgentKinds ?? [])
|
|
102
|
+
// Seed the connect form's backend-kind selectors (built-in + any custom backend a
|
|
103
|
+
// deployment registered), so a programmatically-registered env/runner backend is a
|
|
104
|
+
// first-class connect option instead of a hardcoded manifest/kubernetes list.
|
|
105
|
+
useProviderConnectionsStore().registerBackendKinds({
|
|
106
|
+
environment: snapshot.environmentBackendKinds,
|
|
107
|
+
'runner-pool': snapshot.runnerBackendKinds,
|
|
108
|
+
})
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
/** Resolve accounts + boards, then open the right board for the active account. */
|
|
@@ -50,8 +50,14 @@ export interface ProviderConnection {
|
|
|
50
50
|
*/
|
|
51
51
|
export interface RegisterProviderInput {
|
|
52
52
|
manifest?: Record<string, unknown>
|
|
53
|
-
/** The discriminated runner-backend config (manifest pool or
|
|
53
|
+
/** The discriminated runner-backend config (manifest pool, kubernetes, or a custom kind). */
|
|
54
54
|
config?: Record<string, unknown>
|
|
55
|
+
/**
|
|
56
|
+
* The selected backend kind, used to wrap a bare `manifest` into the discriminated config
|
|
57
|
+
* (`{ kind, manifest }`). Defaults to `manifest`. A CUSTOM registered kind passes its slug
|
|
58
|
+
* here so the flat-form save isn't mis-tagged as the built-in manifest backend.
|
|
59
|
+
*/
|
|
60
|
+
backendKind?: string
|
|
55
61
|
secrets: Record<string, string>
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -59,5 +65,6 @@ export interface RegisterProviderInput {
|
|
|
59
65
|
export interface TestProviderInput {
|
|
60
66
|
manifest?: Record<string, unknown>
|
|
61
67
|
config?: Record<string, unknown>
|
|
68
|
+
backendKind?: string
|
|
62
69
|
secrets?: Record<string, string>
|
|
63
70
|
}
|
package/i18n/locales/en.json
CHANGED
|
@@ -1275,13 +1275,24 @@
|
|
|
1275
1275
|
"executionBackend": {
|
|
1276
1276
|
"label": "Where agents run",
|
|
1277
1277
|
"local-docker": "Local Docker (host)",
|
|
1278
|
+
"local-dockerDesc": "Run each agent in a container on this machine's Docker daemon.",
|
|
1278
1279
|
"cloudflare-containers": "Cloudflare Containers (built-in)",
|
|
1279
|
-
"
|
|
1280
|
+
"cloudflare-containersDesc": "Run agents on Cloudflare's built-in per-run container platform.",
|
|
1281
|
+
"kubernetes": "Kubernetes cluster",
|
|
1282
|
+
"kubernetesDesc": "Run each agent as a pod in a Kubernetes cluster you operate.",
|
|
1283
|
+
"runner-pool": "Custom runner pool (HTTP)",
|
|
1284
|
+
"runner-poolDesc": "Dispatch agents to your own scheduler behind an HTTP manifest API.",
|
|
1285
|
+
"k3s": "Local Kubernetes (k3s)",
|
|
1286
|
+
"k3sDesc": "Prefilled for a local k3s/k3d cluster on this machine — just paste a token."
|
|
1280
1287
|
},
|
|
1281
1288
|
"testEnvBackend": {
|
|
1282
1289
|
"label": "Where test environments run",
|
|
1283
1290
|
"local-compose": "In-container docker-compose",
|
|
1284
|
-
"
|
|
1291
|
+
"local-composeDesc": "Stand the Tester's dependencies up with docker-compose inside the run's container.",
|
|
1292
|
+
"kubernetes": "Kubernetes cluster",
|
|
1293
|
+
"kubernetesDesc": "Provision a per-PR namespace in a Kubernetes cluster you operate.",
|
|
1294
|
+
"environment-provider": "Custom HTTP provider",
|
|
1295
|
+
"environment-providerDesc": "Provision ephemeral environments through your own HTTP management API."
|
|
1285
1296
|
}
|
|
1286
1297
|
},
|
|
1287
1298
|
"providerConnection": {
|
|
@@ -1302,13 +1313,6 @@
|
|
|
1302
1313
|
"blurb": "Where the coding agents run when not using Cloudflare Containers. Choose a self-hosted runner pool (your own scheduler) or a Kubernetes cluster, then configure its endpoint and credentials."
|
|
1303
1314
|
}
|
|
1304
1315
|
},
|
|
1305
|
-
"backend": {
|
|
1306
|
-
"selectorLabel": "Runner backend",
|
|
1307
|
-
"manifest": "Self-hosted pool (manifest)",
|
|
1308
|
-
"kubernetes": "Kubernetes",
|
|
1309
|
-
"environmentSelectorLabel": "Environment backend",
|
|
1310
|
-
"environmentManifest": "Custom HTTP API (manifest)"
|
|
1311
|
-
},
|
|
1312
1316
|
"kubernetesEnv": {
|
|
1313
1317
|
"label": "Name",
|
|
1314
1318
|
"labelPlaceholder": "Preview cluster",
|
|
@@ -1359,6 +1363,8 @@
|
|
|
1359
1363
|
"apiTokenHelp": "A bearer token with RBAC to create, get and delete pods and pods/proxy in the namespace. Stored encrypted; never shown again.",
|
|
1360
1364
|
"caCertPem": "Cluster CA certificate (PEM)",
|
|
1361
1365
|
"caCertPemHelp": "Paste the cluster CA bundle so the apiserver's TLS certificate verifies. Omit only for a publicly-trusted CA.",
|
|
1366
|
+
"insecureSkipTlsVerify": "Skip TLS verification",
|
|
1367
|
+
"insecureSkipTlsVerifyHelp": "Strongly discouraged. Disables apiserver TLS verification; use only for local k3s/kind/dev clusters.",
|
|
1362
1368
|
"harnessPort": "Harness port"
|
|
1363
1369
|
},
|
|
1364
1370
|
"manifestEditor": {
|
|
@@ -1370,13 +1376,10 @@
|
|
|
1370
1376
|
"schemaError": "Manifest problem: {message}",
|
|
1371
1377
|
"secretsLabel": "Secrets",
|
|
1372
1378
|
"noSecrets": "This manifest references no secrets.",
|
|
1379
|
+
"stored": "Currently stored: {keys}",
|
|
1373
1380
|
"reenterSecrets": "Re-enter every secret to save. Stored secrets are write-only and aren't shown.",
|
|
1374
1381
|
"starterHint": "This is a starter example. Edit it to match your provider's API."
|
|
1375
1382
|
},
|
|
1376
|
-
"advancedManifest": {
|
|
1377
|
-
"summary": "Advanced: custom API-based scheduler",
|
|
1378
|
-
"intro": "Only needed to integrate a custom API-based scheduler. The common backends (local Docker, Cloudflare Containers and Kubernetes) don't need this; describe your own scheduler's HTTP API here only if you run one."
|
|
1379
|
-
},
|
|
1380
1383
|
"viewLogs": "View logs",
|
|
1381
1384
|
"hideLogs": "Hide logs",
|
|
1382
1385
|
"connectedAt": "Connected · {baseUrl}",
|
package/i18n/locales/es.json
CHANGED
|
@@ -1256,6 +1256,7 @@
|
|
|
1256
1256
|
"schemaError": "Problema con el manifiesto — {message}",
|
|
1257
1257
|
"secretsLabel": "Secretos",
|
|
1258
1258
|
"noSecrets": "Este manifiesto no hace referencia a ningún secreto.",
|
|
1259
|
+
"stored": "Almacenados actualmente: {keys}",
|
|
1259
1260
|
"reenterSecrets": "Vuelve a introducir cada secreto para guardar: los secretos almacenados son de solo escritura y no se muestran.",
|
|
1260
1261
|
"starterHint": "Este es un ejemplo inicial. Edítalo para que coincida con la API de tu proveedor."
|
|
1261
1262
|
},
|
|
@@ -1283,13 +1284,6 @@
|
|
|
1283
1284
|
"removed": "Conexión eliminada",
|
|
1284
1285
|
"removeFailed": "No se pudo eliminar la conexión"
|
|
1285
1286
|
},
|
|
1286
|
-
"backend": {
|
|
1287
|
-
"selectorLabel": "Backend de ejecución",
|
|
1288
|
-
"manifest": "Pool autohospedado (manifiesto)",
|
|
1289
|
-
"kubernetes": "Kubernetes",
|
|
1290
|
-
"environmentSelectorLabel": "Backend de entorno",
|
|
1291
|
-
"environmentManifest": "API HTTP personalizada (manifiesto)"
|
|
1292
|
-
},
|
|
1293
1287
|
"kubernetesEnv": {
|
|
1294
1288
|
"label": "Nombre",
|
|
1295
1289
|
"labelPlaceholder": "Clúster de vista previa",
|
|
@@ -1340,11 +1334,9 @@
|
|
|
1340
1334
|
"apiTokenHelp": "Un token bearer con permisos RBAC para crear, obtener y eliminar pods y pods/proxy en el namespace. Se almacena cifrado; no se vuelve a mostrar.",
|
|
1341
1335
|
"caCertPem": "Certificado CA del clúster (PEM)",
|
|
1342
1336
|
"caCertPemHelp": "Pega el bundle CA del clúster para que el certificado TLS del apiserver se verifique. Omítelo solo para una CA de confianza pública.",
|
|
1337
|
+
"insecureSkipTlsVerify": "Omitir verificación TLS",
|
|
1338
|
+
"insecureSkipTlsVerifyHelp": "Muy desaconsejado. Desactiva la verificación TLS del apiserver; úsalo solo para clústeres locales k3s/kind/dev.",
|
|
1343
1339
|
"harnessPort": "Puerto del harness"
|
|
1344
|
-
},
|
|
1345
|
-
"advancedManifest": {
|
|
1346
|
-
"summary": "Avanzado: planificador personalizado basado en API",
|
|
1347
|
-
"intro": "Solo es necesario para integrar un planificador personalizado basado en API. Los backends habituales (Docker local, Cloudflare Containers y Kubernetes) no lo necesitan; describe aquí la API HTTP de tu propio planificador solo si usas uno."
|
|
1348
1340
|
}
|
|
1349
1341
|
},
|
|
1350
1342
|
"serviceFragmentDefaults": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -1256,6 +1256,7 @@
|
|
|
1256
1256
|
"schemaError": "Problème de manifeste — {message}",
|
|
1257
1257
|
"secretsLabel": "Secrets",
|
|
1258
1258
|
"noSecrets": "Ce manifeste ne référence aucun secret.",
|
|
1259
|
+
"stored": "Actuellement stockés : {keys}",
|
|
1259
1260
|
"reenterSecrets": "Saisissez à nouveau chaque secret pour enregistrer : les secrets stockés sont en écriture seule et ne sont pas affichés.",
|
|
1260
1261
|
"starterHint": "Ceci est un exemple de départ. Modifiez-le pour qu'il corresponde à l'API de votre fournisseur."
|
|
1261
1262
|
},
|
|
@@ -1283,13 +1284,6 @@
|
|
|
1283
1284
|
"removed": "Connexion supprimée",
|
|
1284
1285
|
"removeFailed": "Impossible de supprimer la connexion"
|
|
1285
1286
|
},
|
|
1286
|
-
"backend": {
|
|
1287
|
-
"selectorLabel": "Backend d'exécution",
|
|
1288
|
-
"manifest": "Pool auto-hébergé (manifeste)",
|
|
1289
|
-
"kubernetes": "Kubernetes",
|
|
1290
|
-
"environmentSelectorLabel": "Backend d'environnement",
|
|
1291
|
-
"environmentManifest": "API HTTP personnalisée (manifeste)"
|
|
1292
|
-
},
|
|
1293
1287
|
"kubernetesEnv": {
|
|
1294
1288
|
"label": "Nom",
|
|
1295
1289
|
"labelPlaceholder": "Cluster de prévisualisation",
|
|
@@ -1340,11 +1334,9 @@
|
|
|
1340
1334
|
"apiTokenHelp": "Un jeton bearer avec les droits RBAC pour créer, lire et supprimer les pods et pods/proxy dans le namespace. Stocké chiffré ; jamais réaffiché.",
|
|
1341
1335
|
"caCertPem": "Certificat CA du cluster (PEM)",
|
|
1342
1336
|
"caCertPemHelp": "Collez le bundle CA du cluster pour que le certificat TLS de l'apiserver soit vérifié. À omettre uniquement pour une CA publiquement approuvée.",
|
|
1337
|
+
"insecureSkipTlsVerify": "Ignorer la vérification TLS",
|
|
1338
|
+
"insecureSkipTlsVerifyHelp": "Fortement déconseillé. Désactive la vérification TLS de l'apiserver ; à utiliser uniquement pour des clusters locaux k3s/kind/dev.",
|
|
1343
1339
|
"harnessPort": "Port du harness"
|
|
1344
|
-
},
|
|
1345
|
-
"advancedManifest": {
|
|
1346
|
-
"summary": "Avancé : planificateur personnalisé basé sur une API",
|
|
1347
|
-
"intro": "Nécessaire uniquement pour intégrer un planificateur personnalisé basé sur une API. Les backends courants (Docker local, Cloudflare Containers et Kubernetes) n'en ont pas besoin ; décrivez ici l'API HTTP de votre propre planificateur uniquement si vous en utilisez un."
|
|
1348
1340
|
}
|
|
1349
1341
|
},
|
|
1350
1342
|
"serviceFragmentDefaults": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -1256,6 +1256,7 @@
|
|
|
1256
1256
|
"schemaError": "Problem z manifestem — {message}",
|
|
1257
1257
|
"secretsLabel": "Sekrety",
|
|
1258
1258
|
"noSecrets": "Ten manifest nie odwołuje się do żadnych sekretów.",
|
|
1259
|
+
"stored": "Obecnie przechowywane: {keys}",
|
|
1259
1260
|
"reenterSecrets": "Wprowadź ponownie każdy sekret, aby zapisać — przechowywane sekrety są tylko do zapisu i nie są wyświetlane.",
|
|
1260
1261
|
"starterHint": "To jest przykład startowy. Zmodyfikuj go, aby pasował do API Twojego dostawcy."
|
|
1261
1262
|
},
|
|
@@ -1283,13 +1284,6 @@
|
|
|
1283
1284
|
"removed": "Połączenie usunięte",
|
|
1284
1285
|
"removeFailed": "Nie udało się usunąć połączenia"
|
|
1285
1286
|
},
|
|
1286
|
-
"backend": {
|
|
1287
|
-
"selectorLabel": "Backend wykonawczy",
|
|
1288
|
-
"manifest": "Własny pool (manifest)",
|
|
1289
|
-
"kubernetes": "Kubernetes",
|
|
1290
|
-
"environmentSelectorLabel": "Backend środowiska",
|
|
1291
|
-
"environmentManifest": "Własne API HTTP (manifest)"
|
|
1292
|
-
},
|
|
1293
1287
|
"kubernetesEnv": {
|
|
1294
1288
|
"label": "Nazwa",
|
|
1295
1289
|
"labelPlaceholder": "Klaster podglądu",
|
|
@@ -1340,11 +1334,9 @@
|
|
|
1340
1334
|
"apiTokenHelp": "Token bearer z uprawnieniami RBAC do tworzenia, odczytu i usuwania podów oraz pods/proxy w namespace. Przechowywany w postaci zaszyfrowanej; nie jest ponownie pokazywany.",
|
|
1341
1335
|
"caCertPem": "Certyfikat CA klastra (PEM)",
|
|
1342
1336
|
"caCertPemHelp": "Wklej pakiet CA klastra, aby certyfikat TLS apiservera został zweryfikowany. Pomiń tylko dla publicznie zaufanego CA.",
|
|
1337
|
+
"insecureSkipTlsVerify": "Pomiń weryfikację TLS",
|
|
1338
|
+
"insecureSkipTlsVerifyHelp": "Zdecydowanie odradzane. Wyłącza weryfikację TLS apiservera; używaj tylko dla lokalnych klastrów k3s/kind/dev.",
|
|
1343
1339
|
"harnessPort": "Port harnessa"
|
|
1344
|
-
},
|
|
1345
|
-
"advancedManifest": {
|
|
1346
|
-
"summary": "Zaawansowane: niestandardowy harmonogram oparty na API",
|
|
1347
|
-
"intro": "Potrzebne tylko do zintegrowania niestandardowego harmonogramu opartego na API. Typowe backendy (lokalny Docker, Cloudflare Containers i Kubernetes) tego nie wymagają; opisz tutaj API HTTP własnego harmonogramu tylko, jeśli go używasz."
|
|
1348
1340
|
}
|
|
1349
1341
|
},
|
|
1350
1342
|
"serviceFragmentDefaults": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -1256,6 +1256,7 @@
|
|
|
1256
1256
|
"schemaError": "Проблема з маніфестом — {message}",
|
|
1257
1257
|
"secretsLabel": "Секрети",
|
|
1258
1258
|
"noSecrets": "Цей маніфест не посилається на жодні секрети.",
|
|
1259
|
+
"stored": "Зараз збережено: {keys}",
|
|
1259
1260
|
"reenterSecrets": "Введіть кожен секрет повторно, щоб зберегти — збережені секрети доступні лише для запису й не показуються.",
|
|
1260
1261
|
"starterHint": "Це початковий приклад. Відредагуйте його відповідно до API вашого постачальника."
|
|
1261
1262
|
},
|
|
@@ -1283,13 +1284,6 @@
|
|
|
1283
1284
|
"removed": "Підключення видалено",
|
|
1284
1285
|
"removeFailed": "Не вдалося видалити підключення"
|
|
1285
1286
|
},
|
|
1286
|
-
"backend": {
|
|
1287
|
-
"selectorLabel": "Бекенд виконавця",
|
|
1288
|
-
"manifest": "Власний пул (маніфест)",
|
|
1289
|
-
"kubernetes": "Kubernetes",
|
|
1290
|
-
"environmentSelectorLabel": "Бекенд середовища",
|
|
1291
|
-
"environmentManifest": "Власний HTTP API (маніфест)"
|
|
1292
|
-
},
|
|
1293
1287
|
"kubernetesEnv": {
|
|
1294
1288
|
"label": "Назва",
|
|
1295
1289
|
"labelPlaceholder": "Кластер попереднього перегляду",
|
|
@@ -1340,11 +1334,9 @@
|
|
|
1340
1334
|
"apiTokenHelp": "Bearer-токен з правами RBAC на створення, читання та видалення подів і pods/proxy у просторі імен. Зберігається зашифрованим; більше не показується.",
|
|
1341
1335
|
"caCertPem": "Сертифікат CA кластера (PEM)",
|
|
1342
1336
|
"caCertPemHelp": "Вставте CA-набір кластера, щоб TLS-сертифікат apiserver проходив перевірку. Пропустіть лише для публічно довіреного CA.",
|
|
1337
|
+
"insecureSkipTlsVerify": "Пропустити перевірку TLS",
|
|
1338
|
+
"insecureSkipTlsVerifyHelp": "Наполегливо не рекомендується. Вимикає перевірку TLS apiserver; використовуйте лише для локальних кластерів k3s/kind/dev.",
|
|
1343
1339
|
"harnessPort": "Порт harness"
|
|
1344
|
-
},
|
|
1345
|
-
"advancedManifest": {
|
|
1346
|
-
"summary": "Розширено: власний планувальник на основі API",
|
|
1347
|
-
"intro": "Потрібно лише для інтеграції власного планувальника на основі API. Звичайні бекенди (локальний Docker, Cloudflare Containers і Kubernetes) цього не потребують; опишіть тут HTTP API власного планувальника, лише якщо ви його використовуєте."
|
|
1348
1340
|
}
|
|
1349
1341
|
},
|
|
1350
1342
|
"serviceFragmentDefaults": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"valibot": "^1.4.2",
|
|
35
35
|
"vue": "^3.5.39",
|
|
36
36
|
"wretch": "^3.0.9",
|
|
37
|
-
"@cat-factory/contracts": "0.
|
|
37
|
+
"@cat-factory/contracts": "0.56.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// A clear picker for WHERE work runs, replacing the old bare "delegate to runner pool"
|
|
3
|
-
// yes/no switch. It reads the deployment's available backends from the capability descriptor
|
|
4
|
-
// (`auth.infrastructure`) and presents the actual options for THIS deployment:
|
|
5
|
-
// - execution axis → where agent containers run (local Docker host / Cloudflare Containers /
|
|
6
|
-
// self-hosted runner pool);
|
|
7
|
-
// - testEnv axis → where the Tester's ephemeral environments run (in-container
|
|
8
|
-
// docker-compose / an environment provider).
|
|
9
|
-
//
|
|
10
|
-
// The control is WRITABLE only in local mode, where the choice is a real per-workspace toggle
|
|
11
|
-
// (the `delegateAgentsToRunnerPool` / `delegateTestEnvToProvider` settings — this selector is a
|
|
12
|
-
// nicer face over those existing booleans, no new persistence). On the Worker/Node facades the
|
|
13
|
-
// active backend is determined by the deployment + whether a pool is registered (the Worker
|
|
14
|
-
// routes to a registered pool automatically, else Cloudflare Containers), so there it renders a
|
|
15
|
-
// read-only "Active: …" line instead of a control that wouldn't actually switch anything.
|
|
16
|
-
import { computed, ref } from 'vue'
|
|
17
|
-
import type { ExecutionBackendKind, TestEnvBackendKind } from '@cat-factory/contracts'
|
|
18
|
-
|
|
19
|
-
const props = defineProps<{ axis: 'execution' | 'testEnv' }>()
|
|
20
|
-
|
|
21
|
-
const { t } = useI18n()
|
|
22
|
-
const auth = useAuthStore()
|
|
23
|
-
const settings = useWorkspaceSettingsStore()
|
|
24
|
-
const providerConnections = useProviderConnectionsStore()
|
|
25
|
-
const toast = useToast()
|
|
26
|
-
|
|
27
|
-
type BackendKind = ExecutionBackendKind | TestEnvBackendKind
|
|
28
|
-
|
|
29
|
-
// Per-axis labels — static literal keys whose leaves mirror the contract enum values verbatim
|
|
30
|
-
// (so the typed-message-keys check stays live and a dynamic lookup is total).
|
|
31
|
-
const EXECUTION_LABELS: Record<ExecutionBackendKind, string> = {
|
|
32
|
-
'local-docker': 'settings.infrastructure.executionBackend.local-docker',
|
|
33
|
-
'cloudflare-containers': 'settings.infrastructure.executionBackend.cloudflare-containers',
|
|
34
|
-
'runner-pool': 'settings.infrastructure.executionBackend.runner-pool',
|
|
35
|
-
}
|
|
36
|
-
const TEST_ENV_LABELS: Record<TestEnvBackendKind, string> = {
|
|
37
|
-
'local-compose': 'settings.infrastructure.testEnvBackend.local-compose',
|
|
38
|
-
'environment-provider': 'settings.infrastructure.testEnvBackend.environment-provider',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function labelFor(kind: BackendKind): string {
|
|
42
|
-
const key =
|
|
43
|
-
props.axis === 'execution'
|
|
44
|
-
? EXECUTION_LABELS[kind as ExecutionBackendKind]
|
|
45
|
-
: TEST_ENV_LABELS[kind as TestEnvBackendKind]
|
|
46
|
-
return t(key)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Normalise to the union element type so array ops (`includes`/`find`) don't collapse to
|
|
50
|
-
// `never` across the execution/testEnv discriminated union.
|
|
51
|
-
const cap = computed<{ available: BackendKind[]; active: BackendKind } | null>(() => {
|
|
52
|
-
const c = auth.infrastructure?.[props.axis]
|
|
53
|
-
return c ? { available: c.available as BackendKind[], active: c.active as BackendKind } : null
|
|
54
|
-
})
|
|
55
|
-
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
56
|
-
|
|
57
|
-
// The backend reached by "delegating" away from the on-machine default, plus the provider
|
|
58
|
-
// connection that must exist for it to work.
|
|
59
|
-
const delegatedKind = computed<BackendKind>(() =>
|
|
60
|
-
props.axis === 'execution' ? 'runner-pool' : 'environment-provider',
|
|
61
|
-
)
|
|
62
|
-
const connectionKind = computed<'runner-pool' | 'environment'>(() =>
|
|
63
|
-
props.axis === 'execution' ? 'runner-pool' : 'environment',
|
|
64
|
-
)
|
|
65
|
-
const delegatedRegistered = computed(
|
|
66
|
-
() => !!providerConnections.connectionFor(connectionKind.value),
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
// The on-machine / built-in default (the available option that isn't the delegated one).
|
|
70
|
-
const localKind = computed<BackendKind>(
|
|
71
|
-
() =>
|
|
72
|
-
cap.value?.available.find((k) => k !== delegatedKind.value) ??
|
|
73
|
-
cap.value?.active ??
|
|
74
|
-
'local-docker',
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
// The delegation flag is a genuine per-workspace toggle ONLY in local mode; elsewhere the
|
|
78
|
-
// active backend is registration/deployment-driven, so the control is read-only there.
|
|
79
|
-
const writable = computed(() => isLocal.value && (cap.value?.available.length ?? 0) > 1)
|
|
80
|
-
|
|
81
|
-
const delegated = computed(() =>
|
|
82
|
-
props.axis === 'execution'
|
|
83
|
-
? settings.settings.delegateAgentsToRunnerPool
|
|
84
|
-
: settings.settings.delegateTestEnvToProvider,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
// The effective active backend. In local mode it follows the delegation flag; otherwise the
|
|
88
|
-
// delegated backend is active when its provider is registered (the Worker auto-routes to a
|
|
89
|
-
// registered pool), else the deployment default from the descriptor.
|
|
90
|
-
const activeKind = computed<BackendKind>(() => {
|
|
91
|
-
if (!cap.value) return localKind.value
|
|
92
|
-
if (writable.value) return delegated.value ? delegatedKind.value : localKind.value
|
|
93
|
-
if (cap.value.available.includes(delegatedKind.value) && delegatedRegistered.value) {
|
|
94
|
-
return delegatedKind.value
|
|
95
|
-
}
|
|
96
|
-
return cap.value.active
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
const saving = ref(false)
|
|
100
|
-
|
|
101
|
-
async function select(kind: BackendKind) {
|
|
102
|
-
if (kind === activeKind.value) return
|
|
103
|
-
const toRunnerPool = kind === delegatedKind.value
|
|
104
|
-
saving.value = true
|
|
105
|
-
try {
|
|
106
|
-
await settings.update(
|
|
107
|
-
props.axis === 'execution'
|
|
108
|
-
? { delegateAgentsToRunnerPool: toRunnerPool }
|
|
109
|
-
: { delegateTestEnvToProvider: toRunnerPool },
|
|
110
|
-
)
|
|
111
|
-
} catch (e) {
|
|
112
|
-
toast.add({
|
|
113
|
-
title: t('settings.infrastructure.updateFailed'),
|
|
114
|
-
description: e instanceof Error ? e.message : String(e),
|
|
115
|
-
icon: 'i-lucide-triangle-alert',
|
|
116
|
-
color: 'error',
|
|
117
|
-
})
|
|
118
|
-
} finally {
|
|
119
|
-
saving.value = false
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const labelKey = computed(() =>
|
|
124
|
-
props.axis === 'execution'
|
|
125
|
-
? 'settings.infrastructure.executionBackend.label'
|
|
126
|
-
: 'settings.infrastructure.testEnvBackend.label',
|
|
127
|
-
)
|
|
128
|
-
</script>
|
|
129
|
-
|
|
130
|
-
<template>
|
|
131
|
-
<section v-if="cap" class="space-y-2 rounded-lg border border-slate-700 bg-slate-900/40 p-3">
|
|
132
|
-
<h3 class="text-sm font-semibold text-slate-200">{{ t(labelKey) }}</h3>
|
|
133
|
-
|
|
134
|
-
<!-- Local mode: a real choice the user can flip (writes the delegation setting). -->
|
|
135
|
-
<div v-if="writable" class="space-y-1.5" :data-testid="`${axis}-backend-options`">
|
|
136
|
-
<label
|
|
137
|
-
v-for="kind in cap.available"
|
|
138
|
-
:key="kind"
|
|
139
|
-
class="flex items-start gap-2"
|
|
140
|
-
:class="
|
|
141
|
-
kind === delegatedKind && !delegatedRegistered
|
|
142
|
-
? 'cursor-not-allowed opacity-50'
|
|
143
|
-
: 'cursor-pointer'
|
|
144
|
-
"
|
|
145
|
-
>
|
|
146
|
-
<input
|
|
147
|
-
type="radio"
|
|
148
|
-
class="mt-1"
|
|
149
|
-
:value="kind"
|
|
150
|
-
:checked="kind === activeKind"
|
|
151
|
-
:disabled="saving || (kind === delegatedKind && !delegatedRegistered)"
|
|
152
|
-
@change="select(kind)"
|
|
153
|
-
/>
|
|
154
|
-
<span class="min-w-0">
|
|
155
|
-
<span class="text-sm text-slate-200">{{ labelFor(kind) }}</span>
|
|
156
|
-
<span
|
|
157
|
-
v-if="kind === delegatedKind && !delegatedRegistered"
|
|
158
|
-
class="block text-[11px] text-amber-300/80"
|
|
159
|
-
>
|
|
160
|
-
{{ t('settings.infrastructure.registerHint') }}
|
|
161
|
-
</span>
|
|
162
|
-
</span>
|
|
163
|
-
</label>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
<!-- Worker/Node: the active backend is deployment/registration-driven, not a toggle. -->
|
|
167
|
-
<p v-else class="text-sm text-slate-300" :data-testid="`${axis}-backend-active`">
|
|
168
|
-
{{ t('settings.infrastructure.active', { backend: labelFor(activeKind) }) }}
|
|
169
|
-
</p>
|
|
170
|
-
</section>
|
|
171
|
-
</template>
|