@cat-factory/app 0.53.0 → 0.54.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/bootstrap/BootstrapModal.vue +81 -69
- package/app/components/environments/EnvironmentStatusPanel.vue +37 -14
- package/app/components/fragments/FragmentLibraryManager.vue +104 -69
- package/app/components/fragments/FragmentLibraryPanel.vue +2 -1
- package/app/components/kaizen/KaizenPanel.vue +36 -25
- package/app/components/kaizen/KaizenStepStatus.vue +13 -10
- package/app/components/media/ArtifactLightbox.vue +20 -14
- package/app/components/media/ImageCompare.vue +35 -22
- package/app/components/provisioning/ProvisioningLogsDrawer.vue +27 -14
- package/app/components/recurring/RecurrenceEditor.vue +22 -15
- package/app/components/sandbox/SandboxPanel.vue +126 -59
- 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 +46 -85
- package/app/components/settings/ProviderManifestEditor.vue +19 -3
- package/i18n/locales/en.json +425 -13
- package/i18n/locales/es.json +403 -11
- package/i18n/locales/fr.json +403 -11
- package/i18n/locales/pl.json +403 -11
- package/i18n/locales/uk.json +403 -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
|
|
|
@@ -109,7 +120,7 @@ function buildManifestPayload(): {
|
|
|
109
120
|
if (Object.keys(providerConfig).length) manifest.providerConfig = providerConfig
|
|
110
121
|
// Carry the selected kind so a CUSTOM backend's flat-form save is tagged with its slug
|
|
111
122
|
// (not silently wrapped into the built-in `manifest` backend).
|
|
112
|
-
return { manifest, secrets, backendKind: backendKind
|
|
123
|
+
return { manifest, secrets, backendKind: props.backendKind }
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
function notifyError(title: string, e: unknown) {
|
|
@@ -149,6 +160,7 @@ async function saveNative() {
|
|
|
149
160
|
try {
|
|
150
161
|
const payload = buildManifestPayload()
|
|
151
162
|
if (payload) await store.register(props.kind, payload)
|
|
163
|
+
emit('connected')
|
|
152
164
|
resetDraft()
|
|
153
165
|
toastSaved()
|
|
154
166
|
} catch (e) {
|
|
@@ -168,7 +180,7 @@ async function testManifest(payload: {
|
|
|
168
180
|
testing.value = true
|
|
169
181
|
testResult.value = null
|
|
170
182
|
try {
|
|
171
|
-
testResult.value = await store.test(props.kind, { ...payload, backendKind: backendKind
|
|
183
|
+
testResult.value = await store.test(props.kind, { ...payload, backendKind: props.backendKind })
|
|
172
184
|
} catch (e) {
|
|
173
185
|
testResult.value = { ok: false, message: e instanceof Error ? e.message : String(e) }
|
|
174
186
|
} finally {
|
|
@@ -182,7 +194,8 @@ async function saveManifest(payload: {
|
|
|
182
194
|
}) {
|
|
183
195
|
busy.value = true
|
|
184
196
|
try {
|
|
185
|
-
await store.register(props.kind, { ...payload, backendKind: backendKind
|
|
197
|
+
await store.register(props.kind, { ...payload, backendKind: props.backendKind })
|
|
198
|
+
emit('connected')
|
|
186
199
|
toastSaved()
|
|
187
200
|
} catch (e) {
|
|
188
201
|
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
@@ -191,54 +204,20 @@ async function saveManifest(payload: {
|
|
|
191
204
|
}
|
|
192
205
|
}
|
|
193
206
|
|
|
194
|
-
// --- Backend
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
props.kind === 'environment'
|
|
204
|
-
? 'settings.providerConnection.backend.environmentSelectorLabel'
|
|
205
|
-
: 'settings.providerConnection.backend.selectorLabel',
|
|
206
|
-
),
|
|
207
|
-
)
|
|
208
|
-
// Built-in kinds keep their localized labels; a custom kind shows its snapshot displayLabel.
|
|
209
|
-
function backendKindLabel(option: { kind: string; label: string }): string {
|
|
210
|
-
if (option.kind === 'kubernetes') return t('settings.providerConnection.backend.kubernetes')
|
|
211
|
-
if (option.kind === 'manifest') {
|
|
212
|
-
return t(
|
|
213
|
-
props.kind === 'environment'
|
|
214
|
-
? 'settings.providerConnection.backend.environmentManifest'
|
|
215
|
-
: 'settings.providerConnection.backend.manifest',
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
return option.label
|
|
219
|
-
}
|
|
220
|
-
const backendKindItems = computed(() =>
|
|
221
|
-
store.backendKindsFor(props.kind).map((o) => ({ label: backendKindLabel(o), value: o.kind })),
|
|
222
|
-
)
|
|
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.
|
|
223
216
|
watch(
|
|
224
|
-
() =>
|
|
225
|
-
(
|
|
226
|
-
if (c?.kind) backendKind.value = c.kind
|
|
227
|
-
},
|
|
228
|
-
{ immediate: true },
|
|
217
|
+
() => props.backendKind,
|
|
218
|
+
(k) => void store.loadDescriptor(props.kind, k).then(resetDraft),
|
|
229
219
|
)
|
|
230
220
|
|
|
231
|
-
// Switching the backend kind re-probes ONLY that kind's descriptor (so a not-yet-connected
|
|
232
|
-
// custom kind's connect form renders). Always pass the explicit kind — including `manifest`,
|
|
233
|
-
// so picking it describes the manifest backend rather than falling back to the stored kind —
|
|
234
|
-
// and use `loadDescriptor` (not `loadKind`) so the stored connection isn't re-fetched and the
|
|
235
|
-
// selector isn't bounced back to the stored kind by the `connection` watch.
|
|
236
|
-
async function onBackendKindChange(k: string) {
|
|
237
|
-
backendKind.value = k
|
|
238
|
-
await store.loadDescriptor(props.kind, k)
|
|
239
|
-
resetDraft()
|
|
240
|
-
}
|
|
241
|
-
|
|
242
221
|
async function testConfig(payload: {
|
|
243
222
|
config: Record<string, unknown>
|
|
244
223
|
secrets: Record<string, string>
|
|
@@ -261,6 +240,7 @@ async function saveConfig(payload: {
|
|
|
261
240
|
busy.value = true
|
|
262
241
|
try {
|
|
263
242
|
await store.register(props.kind, payload)
|
|
243
|
+
emit('connected')
|
|
264
244
|
toastSaved()
|
|
265
245
|
} catch (e) {
|
|
266
246
|
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
@@ -350,20 +330,12 @@ function fieldHelp(key: string): string | undefined {
|
|
|
350
330
|
}}
|
|
351
331
|
</div>
|
|
352
332
|
|
|
353
|
-
<!-- Backend selector: the BYO manifest backend, a native Kubernetes backend, or a
|
|
354
|
-
programmatically-registered custom kind. -->
|
|
355
|
-
<UFormField :label="backendSelectorLabel">
|
|
356
|
-
<USelect
|
|
357
|
-
v-model="backendKind"
|
|
358
|
-
:items="backendKindItems"
|
|
359
|
-
@update:model-value="onBackendKindChange(String($event))"
|
|
360
|
-
/>
|
|
361
|
-
</UFormField>
|
|
362
|
-
|
|
363
333
|
<!-- Native Kubernetes runner backend (runner-pool). -->
|
|
364
334
|
<KubernetesRunnerForm
|
|
365
335
|
v-if="kind === 'runner-pool' && backendKind === 'kubernetes'"
|
|
366
336
|
:connection="connection"
|
|
337
|
+
:preset="preset"
|
|
338
|
+
:suggested-image="suggestedImage"
|
|
367
339
|
:supports-test="descriptor.supportsTest"
|
|
368
340
|
:testing="testing"
|
|
369
341
|
:busy="busy"
|
|
@@ -463,31 +435,20 @@ function fieldHelp(key: string): string | undefined {
|
|
|
463
435
|
</div>
|
|
464
436
|
</div>
|
|
465
437
|
|
|
466
|
-
<!-- MANIFEST-driven provider: the raw JSON manifest editor.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<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
|
|
470
441
|
v-else
|
|
471
|
-
|
|
472
|
-
:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
:saved-manifest="descriptor.savedManifest"
|
|
483
|
-
:connected="!!connection"
|
|
484
|
-
:supports-test="descriptor.supportsTest"
|
|
485
|
-
:testing="testing"
|
|
486
|
-
:busy="busy"
|
|
487
|
-
:test-result="testResult"
|
|
488
|
-
@test="testManifest"
|
|
489
|
-
@save="saveManifest"
|
|
490
|
-
/>
|
|
491
|
-
</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
|
+
/>
|
|
492
453
|
</div>
|
|
493
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]"
|