@cat-factory/app 0.90.0 → 0.91.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/board/RecurringPipelineModal.vue +1 -0
- package/app/components/settings/KubernetesEnvironmentForm.vue +5 -3
- package/app/components/settings/ProviderConnectionTab.vue +115 -28
- package/app/utils/catalog.ts +13 -0
- package/package.json +2 -2
- package/app/components/settings/KubernetesRunnerForm.vue +0 -252
|
@@ -86,6 +86,7 @@ const selectedPipelineLabel = computed(
|
|
|
86
86
|
const template = computed<ScheduleTemplate>(() => {
|
|
87
87
|
if (pipelineId.value === 'pl_tech_debt') return 'tech-debt'
|
|
88
88
|
if (pipelineId.value === 'pl_dep_update') return 'dep-update'
|
|
89
|
+
if (pipelineId.value === 'pl_bug_triage') return 'bug-triage'
|
|
89
90
|
return 'custom'
|
|
90
91
|
})
|
|
91
92
|
const isTechDebt = computed(() => template.value === 'tech-debt')
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// environment tab's backend-type selector (the other being the BYO HTTP manifest editor).
|
|
4
4
|
// It builds the discriminated `{ kind: 'kubernetes', kubernetes }` config plus the
|
|
5
5
|
// `apiToken` secret bundle and emits test/save to the parent tab (which calls the shared
|
|
6
|
-
// provider-connections store). The K8s env config
|
|
7
|
-
// (
|
|
8
|
-
//
|
|
6
|
+
// provider-connections store). The K8s env config has manifest-source + URL-derivation
|
|
7
|
+
// (discriminated-union) fields a flat descriptor form can't yet express, so — unlike the
|
|
8
|
+
// runner backends, which now self-describe via `configTemplate` and render through the
|
|
9
|
+
// generic form — the environment axis keeps this bespoke form until it, too, is descriptor-
|
|
10
|
+
// driven (see docs/initiatives/descriptor-driven-infra-forms.md).
|
|
9
11
|
import { computed, reactive, ref, watch } from 'vue'
|
|
10
12
|
import { KUBERNETES_ENV_TOKEN_SECRET_KEY } from '@cat-factory/contracts'
|
|
11
13
|
import type { ProviderConnection } from '~/types/providerConnections'
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// One tab of the Infrastructure window — the connect surface for a single provider kind
|
|
3
3
|
// (container agents → runner pool, or test environments → environment provider). Both
|
|
4
|
-
// self-describe via a ProviderDescriptor, so this renders
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// self-describe via a ProviderDescriptor, so this renders every backend WITHOUT hard-coding
|
|
5
|
+
// which optional kinds (Kubernetes, EKS, a custom kind) exist:
|
|
6
|
+
// - a NATIVE MANIFEST provider (ships a `manifestTemplate`) → the flat field form, whose
|
|
7
|
+
// values are overlaid back onto the manifest before saving.
|
|
8
|
+
// - a NATIVE CONFIG backend (ships a `configTemplate` — the runner backends Kubernetes/EKS
|
|
9
|
+
// and any custom native kind) → the SAME flat field form, whose values are overlaid onto
|
|
10
|
+
// the discriminated `{ kind, <payload> }` config the backend described. The SPA never
|
|
11
|
+
// names a backend; it reads the single payload key off the skeleton.
|
|
7
12
|
// - a MANIFEST-driven provider (no template) → the full JSON manifest editor
|
|
8
13
|
// (ProviderManifestEditor), which replaces the old "use the API" disclaimer.
|
|
9
14
|
import { computed, ref, toRaw, watch } from 'vue'
|
|
10
|
-
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
15
|
+
import type { ProviderConfigField, ProviderConnectionKind } from '~/types/providerConnections'
|
|
11
16
|
import ProvisioningLogsDrawer from '~/components/provisioning/ProvisioningLogsDrawer.vue'
|
|
12
17
|
import ProviderManifestEditor from '~/components/settings/ProviderManifestEditor.vue'
|
|
13
|
-
import KubernetesRunnerForm from '~/components/settings/KubernetesRunnerForm.vue'
|
|
14
18
|
import KubernetesEnvironmentForm from '~/components/settings/KubernetesEnvironmentForm.vue'
|
|
15
19
|
|
|
16
20
|
const props = defineProps<{
|
|
@@ -56,17 +60,28 @@ const testResult = ref<{ ok: boolean; message?: string } | null>(null)
|
|
|
56
60
|
const testing = ref(false)
|
|
57
61
|
const busy = ref(false)
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
// A native provider renders the friendly flat field form. Two flavours self-describe it:
|
|
64
|
+
// `manifestTemplate` (overlay onto a manifest) and `configTemplate` (overlay onto a
|
|
65
|
+
// discriminated backend config — the Kubernetes/EKS/custom runner backends).
|
|
66
|
+
const isNativeManifest = computed(() => !!descriptor.value?.manifestTemplate)
|
|
67
|
+
const isNativeConfig = computed(() => !!descriptor.value?.configTemplate)
|
|
68
|
+
const isNative = computed(() => isNativeManifest.value || isNativeConfig.value)
|
|
61
69
|
const secretFieldCount = computed(
|
|
62
70
|
() => (descriptor.value?.configFields ?? []).filter((f) => f.secret).length,
|
|
63
71
|
)
|
|
64
72
|
const hasSecretFields = computed(() => secretFieldCount.value > 0)
|
|
65
73
|
|
|
66
|
-
// Seed the flat-form draft from the
|
|
67
|
-
//
|
|
74
|
+
// Seed the flat-form draft from the CURRENT non-secret config so an edit starts populated.
|
|
75
|
+
// Secret fields are never prefilled. A `configTemplate` backend gets its flat values straight
|
|
76
|
+
// from `descriptor.values`; a `manifestTemplate` backend reads them off the saved manifest
|
|
77
|
+
// (baseUrl + providerConfig). On a fresh Kubernetes connect a `k3s` preset prefills local defaults.
|
|
68
78
|
function resetDraft() {
|
|
69
79
|
testResult.value = null
|
|
80
|
+
if (isNativeConfig.value) {
|
|
81
|
+
values.value = { ...descriptor.value?.values }
|
|
82
|
+
applyPresetDefaults()
|
|
83
|
+
return
|
|
84
|
+
}
|
|
70
85
|
const saved = descriptor.value?.savedManifest
|
|
71
86
|
const cfg = (saved?.providerConfig as Record<string, unknown> | undefined) ?? {}
|
|
72
87
|
const next: Record<string, string> = {}
|
|
@@ -82,6 +97,24 @@ function resetDraft() {
|
|
|
82
97
|
values.value = next
|
|
83
98
|
}
|
|
84
99
|
|
|
100
|
+
// The low-config `k3s` preset (an execution-axis radio the picker synthesises) prefills the
|
|
101
|
+
// local-cluster defaults into the generic Kubernetes runner form. Only on a FRESH connect —
|
|
102
|
+
// never clobbering a stored connection's values. Kept SPA-local because the preset is a UI
|
|
103
|
+
// affordance of the picker, not a backend concept.
|
|
104
|
+
function applyPresetDefaults() {
|
|
105
|
+
if (props.preset !== 'k3s' || connection.value) return
|
|
106
|
+
const seed: Record<string, string> = {
|
|
107
|
+
label: 'Local k3s',
|
|
108
|
+
apiServerUrl: 'https://127.0.0.1:6443',
|
|
109
|
+
namespace: 'cat-factory',
|
|
110
|
+
insecureSkipTlsVerify: 'true',
|
|
111
|
+
}
|
|
112
|
+
if (props.suggestedImage) seed.image = props.suggestedImage
|
|
113
|
+
for (const [k, val] of Object.entries(seed)) {
|
|
114
|
+
if (!(values.value[k] ?? '').trim()) values.value[k] = val
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
85
118
|
/** A flat-form field is satisfied when filled now, or already stored, or it has a default. */
|
|
86
119
|
function satisfied(key: string): boolean {
|
|
87
120
|
const f = descriptor.value?.configFields.find((cf) => cf.key === key)
|
|
@@ -124,6 +157,51 @@ function buildManifestPayload(): {
|
|
|
124
157
|
return { manifest, secrets, backendKind: props.backendKind }
|
|
125
158
|
}
|
|
126
159
|
|
|
160
|
+
/** Coerce a flat string form value to the JSON type the backend config expects for its field. */
|
|
161
|
+
function coerceFieldValue(field: ProviderConfigField, raw: string): unknown {
|
|
162
|
+
if (field.type === 'number') return Number(raw)
|
|
163
|
+
if (field.type === 'checkbox') return raw === 'true'
|
|
164
|
+
return raw
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Overlay the flat field values onto a NATIVE backend's discriminated `configTemplate`. The
|
|
169
|
+
* skeleton is `{ kind, <payload> }`, so every non-secret field is written to the single
|
|
170
|
+
* non-`kind` payload key (typed via the field's `type`), each secret to the write-only bundle,
|
|
171
|
+
* and a cleared field is dropped so it reverts to absent. Because the template is the STORED
|
|
172
|
+
* config on an edit, advanced API-only keys the flat form never renders are preserved.
|
|
173
|
+
*/
|
|
174
|
+
function buildConfigPayload(): {
|
|
175
|
+
config: Record<string, unknown>
|
|
176
|
+
secrets: Record<string, string>
|
|
177
|
+
} | null {
|
|
178
|
+
const template = descriptor.value?.configTemplate
|
|
179
|
+
if (!template) return null
|
|
180
|
+
const config: Record<string, unknown> = structuredClone(toRaw(template))
|
|
181
|
+
const payloadKey = Object.keys(config).find((k) => k !== 'kind')
|
|
182
|
+
if (!payloadKey) return null
|
|
183
|
+
const payload: Record<string, unknown> = {
|
|
184
|
+
...(config[payloadKey] as Record<string, unknown> | undefined),
|
|
185
|
+
}
|
|
186
|
+
const secrets: Record<string, string> = {}
|
|
187
|
+
for (const f of descriptor.value?.configFields ?? []) {
|
|
188
|
+
const raw = (values.value[f.key] ?? '').trim()
|
|
189
|
+
if (f.secret) {
|
|
190
|
+
if (raw) secrets[f.key] = raw
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (!raw) delete payload[f.key]
|
|
194
|
+
else payload[f.key] = coerceFieldValue(f, raw)
|
|
195
|
+
}
|
|
196
|
+
config[payloadKey] = payload
|
|
197
|
+
return { config, secrets }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The payload for the active native flavour (discriminated config or manifest overlay). */
|
|
201
|
+
function buildFlatPayload() {
|
|
202
|
+
return isNativeConfig.value ? buildConfigPayload() : buildManifestPayload()
|
|
203
|
+
}
|
|
204
|
+
|
|
127
205
|
function notifyError(title: string, e: unknown) {
|
|
128
206
|
toast.add({
|
|
129
207
|
title,
|
|
@@ -141,9 +219,9 @@ function toastSaved() {
|
|
|
141
219
|
})
|
|
142
220
|
}
|
|
143
221
|
|
|
144
|
-
// --- Native flat-form actions
|
|
222
|
+
// --- Native flat-form actions (both manifest-overlay and config-overlay flavours) ---
|
|
145
223
|
async function testNative() {
|
|
146
|
-
const payload =
|
|
224
|
+
const payload = buildFlatPayload()
|
|
147
225
|
if (!payload) return
|
|
148
226
|
testing.value = true
|
|
149
227
|
testResult.value = null
|
|
@@ -159,7 +237,7 @@ async function testNative() {
|
|
|
159
237
|
async function saveNative() {
|
|
160
238
|
busy.value = true
|
|
161
239
|
try {
|
|
162
|
-
const payload =
|
|
240
|
+
const payload = buildFlatPayload()
|
|
163
241
|
if (payload) await store.register(props.kind, payload)
|
|
164
242
|
emit('connected')
|
|
165
243
|
resetDraft()
|
|
@@ -332,23 +410,12 @@ function fieldHelp(key: string): string | undefined {
|
|
|
332
410
|
}}
|
|
333
411
|
</div>
|
|
334
412
|
|
|
335
|
-
<!-- Native Kubernetes
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
:preset="preset"
|
|
340
|
-
:suggested-image="suggestedImage"
|
|
341
|
-
:supports-test="descriptor.supportsTest"
|
|
342
|
-
:testing="testing"
|
|
343
|
-
:busy="busy"
|
|
344
|
-
:test-result="testResult"
|
|
345
|
-
@test="testConfig"
|
|
346
|
-
@save="saveConfig"
|
|
347
|
-
/>
|
|
348
|
-
|
|
349
|
-
<!-- Native Kubernetes ephemeral-environment backend (environment). -->
|
|
413
|
+
<!-- Native Kubernetes ephemeral-environment backend (environment). The runner-pool
|
|
414
|
+
Kubernetes/EKS backends now self-describe via `configTemplate` and render through the
|
|
415
|
+
generic flat form below — no per-kind component. The env axis keeps its bespoke form
|
|
416
|
+
until it, too, is descriptor-driven (see docs/initiatives/descriptor-driven-infra-forms.md). -->
|
|
350
417
|
<KubernetesEnvironmentForm
|
|
351
|
-
v-
|
|
418
|
+
v-if="kind === 'environment' && backendKind === 'kubernetes'"
|
|
352
419
|
:connection="connection"
|
|
353
420
|
:supports-test="descriptor.supportsTest"
|
|
354
421
|
:testing="testing"
|
|
@@ -396,6 +463,26 @@ function fieldHelp(key: string): string | undefined {
|
|
|
396
463
|
:items="(field.options ?? []).map((o) => ({ label: o.label, value: o.value }))"
|
|
397
464
|
:placeholder="field.default ?? field.placeholder"
|
|
398
465
|
/>
|
|
466
|
+
<USwitch
|
|
467
|
+
v-else-if="field.type === 'checkbox'"
|
|
468
|
+
:model-value="values[field.key] === 'true'"
|
|
469
|
+
@update:model-value="values[field.key] = $event ? 'true' : 'false'"
|
|
470
|
+
/>
|
|
471
|
+
<UTextarea
|
|
472
|
+
v-else-if="field.type === 'textarea'"
|
|
473
|
+
v-model="values[field.key]"
|
|
474
|
+
:rows="4"
|
|
475
|
+
class="w-full font-mono"
|
|
476
|
+
:placeholder="field.default ?? field.placeholder"
|
|
477
|
+
/>
|
|
478
|
+
<UInput
|
|
479
|
+
v-else-if="field.type === 'number'"
|
|
480
|
+
:model-value="values[field.key] ?? ''"
|
|
481
|
+
type="number"
|
|
482
|
+
class="font-mono"
|
|
483
|
+
:placeholder="field.default ?? field.placeholder"
|
|
484
|
+
@update:model-value="values[field.key] = String($event ?? '')"
|
|
485
|
+
/>
|
|
399
486
|
<UInput
|
|
400
487
|
v-else
|
|
401
488
|
v-model="values[field.key]"
|
package/app/utils/catalog.ts
CHANGED
|
@@ -410,6 +410,19 @@ export const SYSTEM_AGENT_META: Record<string, AgentArchetype> = {
|
|
|
410
410
|
description:
|
|
411
411
|
'Files a tracker ticket (GitHub issue / Jira) from the analysis before work starts.',
|
|
412
412
|
},
|
|
413
|
+
// The inbound dual of `tracker`: a one-shot engine step that PULLS one matching open issue
|
|
414
|
+
// from the schedule's configured tracker board, marks it in-progress with a "taken by
|
|
415
|
+
// cat-factory" comment, and reseeds the recurring block from it. Runs no model; seeded only
|
|
416
|
+
// into the recurring bug-triage pipeline, so it is a display-metadata system kind (like
|
|
417
|
+
// `tracker`), not a palette archetype.
|
|
418
|
+
'bug-intake': {
|
|
419
|
+
kind: 'bug-intake',
|
|
420
|
+
label: 'Bug Intake',
|
|
421
|
+
icon: 'i-lucide-inbox',
|
|
422
|
+
color: '#fb923c',
|
|
423
|
+
description:
|
|
424
|
+
'Pulls one matching open issue from the configured tracker board, marks it in-progress, and seeds the run from it.',
|
|
425
|
+
},
|
|
413
426
|
conflicts: {
|
|
414
427
|
kind: 'conflicts',
|
|
415
428
|
label: 'Conflicts Gate',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.1",
|
|
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.99.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// The Kubernetes "agent runner backend" connect form — one option of the runner-pool tab's
|
|
3
|
-
// backend-type selector (the other being the manifest pool). It builds the discriminated
|
|
4
|
-
// `{ kind: 'kubernetes', kubernetes }` config + the `apiToken` secret bundle and emits
|
|
5
|
-
// test/save to the parent tab (which calls the shared provider-connections store).
|
|
6
|
-
import { computed, reactive, ref, watch } from 'vue'
|
|
7
|
-
import { KUBERNETES_RUNNER_TOKEN_SECRET_KEY } from '@cat-factory/contracts'
|
|
8
|
-
import type { ProviderConnection } from '~/types/providerConnections'
|
|
9
|
-
|
|
10
|
-
const props = defineProps<{
|
|
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
|
|
16
|
-
supportsTest: boolean
|
|
17
|
-
testing: boolean
|
|
18
|
-
busy: boolean
|
|
19
|
-
testResult: { ok: boolean; message?: string } | null
|
|
20
|
-
}>()
|
|
21
|
-
|
|
22
|
-
const emit = defineEmits<{
|
|
23
|
-
test: [payload: { config: Record<string, unknown>; secrets: Record<string, string> }]
|
|
24
|
-
save: [payload: { config: Record<string, unknown>; secrets: Record<string, string> }]
|
|
25
|
-
}>()
|
|
26
|
-
|
|
27
|
-
const { t } = useI18n()
|
|
28
|
-
|
|
29
|
-
const form = reactive({
|
|
30
|
-
label: '',
|
|
31
|
-
apiServerUrl: '',
|
|
32
|
-
namespace: '',
|
|
33
|
-
image: '',
|
|
34
|
-
imageUi: '',
|
|
35
|
-
caCertPem: '',
|
|
36
|
-
harnessPort: '',
|
|
37
|
-
insecureSkipTlsVerify: false,
|
|
38
|
-
})
|
|
39
|
-
const apiToken = ref('')
|
|
40
|
-
|
|
41
|
-
// A registered k8s connection exposes its non-secret config, so prefill every non-secret
|
|
42
|
-
// field from it (never the token — secrets are write-only and re-entered on update). This
|
|
43
|
-
// lets an edit change one field without re-typing the whole form.
|
|
44
|
-
watch(
|
|
45
|
-
() => props.connection,
|
|
46
|
-
(c) => {
|
|
47
|
-
if (c?.kind !== 'kubernetes') return
|
|
48
|
-
form.label = c.label
|
|
49
|
-
form.apiServerUrl = c.baseUrl
|
|
50
|
-
const k =
|
|
51
|
-
c.config && (c.config as { kind?: string }).kind === 'kubernetes'
|
|
52
|
-
? (c.config as { kubernetes: Record<string, unknown> }).kubernetes
|
|
53
|
-
: undefined
|
|
54
|
-
if (k) {
|
|
55
|
-
form.namespace = typeof k.namespace === 'string' ? k.namespace : ''
|
|
56
|
-
form.image = typeof k.image === 'string' ? k.image : ''
|
|
57
|
-
form.imageUi = typeof k.imageUi === 'string' ? k.imageUi : ''
|
|
58
|
-
form.caCertPem = typeof k.caCertPem === 'string' ? k.caCertPem : ''
|
|
59
|
-
form.harnessPort = typeof k.harnessPort === 'number' ? String(k.harnessPort) : ''
|
|
60
|
-
form.insecureSkipTlsVerify = k.insecureSkipTlsVerify === true
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
{ immediate: true },
|
|
64
|
-
)
|
|
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
|
-
|
|
82
|
-
const canSave = computed(
|
|
83
|
-
() =>
|
|
84
|
-
!!form.label.trim() &&
|
|
85
|
-
!!form.apiServerUrl.trim() &&
|
|
86
|
-
!!form.namespace.trim() &&
|
|
87
|
-
!!form.image.trim() &&
|
|
88
|
-
!!apiToken.value.trim(),
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
// Why the Connect button is disabled, surfaced as a red hint next to it so a mandatory-field gap
|
|
92
|
-
// is visible rather than a dead button. Lists the empty required fields by their on-screen label.
|
|
93
|
-
const connectBlockedReason = computed(() => {
|
|
94
|
-
if (canSave.value) return ''
|
|
95
|
-
const missing: string[] = []
|
|
96
|
-
if (!form.label.trim()) missing.push(t('settings.providerConnection.kubernetes.label'))
|
|
97
|
-
if (!form.apiServerUrl.trim())
|
|
98
|
-
missing.push(t('settings.providerConnection.kubernetes.apiServerUrl'))
|
|
99
|
-
if (!form.namespace.trim()) missing.push(t('settings.providerConnection.kubernetes.namespace'))
|
|
100
|
-
if (!form.image.trim()) missing.push(t('settings.providerConnection.kubernetes.image'))
|
|
101
|
-
if (!apiToken.value.trim()) missing.push(t('settings.providerConnection.kubernetes.apiToken'))
|
|
102
|
-
return t('settings.providerConnection.form.missingFields', { fields: missing.join(', ') })
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
function buildPayload(): { config: Record<string, unknown>; secrets: Record<string, string> } {
|
|
106
|
-
const kubernetes: Record<string, unknown> = {
|
|
107
|
-
label: form.label.trim(),
|
|
108
|
-
apiServerUrl: form.apiServerUrl.trim(),
|
|
109
|
-
namespace: form.namespace.trim(),
|
|
110
|
-
image: form.image.trim(),
|
|
111
|
-
}
|
|
112
|
-
if (form.imageUi.trim()) kubernetes.imageUi = form.imageUi.trim()
|
|
113
|
-
if (form.caCertPem.trim()) kubernetes.caCertPem = form.caCertPem.trim()
|
|
114
|
-
if (form.insecureSkipTlsVerify) kubernetes.insecureSkipTlsVerify = true
|
|
115
|
-
const port = Number(form.harnessPort)
|
|
116
|
-
if (form.harnessPort.trim() && Number.isFinite(port)) kubernetes.harnessPort = port
|
|
117
|
-
return {
|
|
118
|
-
config: { kind: 'kubernetes', kubernetes },
|
|
119
|
-
secrets: { [KUBERNETES_RUNNER_TOKEN_SECRET_KEY]: apiToken.value.trim() },
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
</script>
|
|
123
|
-
|
|
124
|
-
<template>
|
|
125
|
-
<div class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3">
|
|
126
|
-
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
127
|
-
{{
|
|
128
|
-
connection?.kind === 'kubernetes'
|
|
129
|
-
? t('settings.providerConnection.form.updateConfiguration')
|
|
130
|
-
: t('settings.providerConnection.form.connect')
|
|
131
|
-
}}
|
|
132
|
-
</p>
|
|
133
|
-
|
|
134
|
-
<UFormField :label="t('settings.providerConnection.kubernetes.label')">
|
|
135
|
-
<UInput
|
|
136
|
-
v-model="form.label"
|
|
137
|
-
:placeholder="t('settings.providerConnection.kubernetes.labelPlaceholder')"
|
|
138
|
-
/>
|
|
139
|
-
</UFormField>
|
|
140
|
-
|
|
141
|
-
<UFormField
|
|
142
|
-
:label="t('settings.providerConnection.kubernetes.apiServerUrl')"
|
|
143
|
-
:help="t('settings.providerConnection.kubernetes.apiServerUrlHelp')"
|
|
144
|
-
>
|
|
145
|
-
<UInput v-model="form.apiServerUrl" class="font-mono" placeholder="https://10.0.0.1:6443" />
|
|
146
|
-
</UFormField>
|
|
147
|
-
|
|
148
|
-
<UFormField :label="t('settings.providerConnection.kubernetes.namespace')">
|
|
149
|
-
<UInput v-model="form.namespace" class="font-mono" placeholder="cat-factory" />
|
|
150
|
-
</UFormField>
|
|
151
|
-
|
|
152
|
-
<UFormField
|
|
153
|
-
:label="t('settings.providerConnection.kubernetes.image')"
|
|
154
|
-
:help="t('settings.providerConnection.kubernetes.imageHelp')"
|
|
155
|
-
>
|
|
156
|
-
<UInput
|
|
157
|
-
v-model="form.image"
|
|
158
|
-
class="font-mono"
|
|
159
|
-
placeholder="ghcr.io/acme/cat-factory-executor:latest"
|
|
160
|
-
/>
|
|
161
|
-
</UFormField>
|
|
162
|
-
|
|
163
|
-
<UFormField
|
|
164
|
-
:label="
|
|
165
|
-
t('settings.providerConnection.form.optionalLabel', {
|
|
166
|
-
label: t('settings.providerConnection.kubernetes.imageUi'),
|
|
167
|
-
})
|
|
168
|
-
"
|
|
169
|
-
>
|
|
170
|
-
<UInput v-model="form.imageUi" class="font-mono" />
|
|
171
|
-
</UFormField>
|
|
172
|
-
|
|
173
|
-
<UFormField
|
|
174
|
-
:label="t('settings.providerConnection.kubernetes.apiToken')"
|
|
175
|
-
:help="t('settings.providerConnection.kubernetes.apiTokenHelp')"
|
|
176
|
-
>
|
|
177
|
-
<UInput v-model="apiToken" type="password" class="font-mono" />
|
|
178
|
-
</UFormField>
|
|
179
|
-
|
|
180
|
-
<UFormField
|
|
181
|
-
:label="
|
|
182
|
-
t('settings.providerConnection.form.optionalLabel', {
|
|
183
|
-
label: t('settings.providerConnection.kubernetes.caCertPem'),
|
|
184
|
-
})
|
|
185
|
-
"
|
|
186
|
-
:help="t('settings.providerConnection.kubernetes.caCertPemHelp')"
|
|
187
|
-
>
|
|
188
|
-
<UTextarea
|
|
189
|
-
v-model="form.caCertPem"
|
|
190
|
-
:rows="3"
|
|
191
|
-
class="font-mono"
|
|
192
|
-
placeholder="-----BEGIN CERTIFICATE-----"
|
|
193
|
-
/>
|
|
194
|
-
</UFormField>
|
|
195
|
-
|
|
196
|
-
<UFormField :help="t('settings.providerConnection.kubernetes.insecureSkipTlsVerifyHelp')">
|
|
197
|
-
<UCheckbox
|
|
198
|
-
v-model="form.insecureSkipTlsVerify"
|
|
199
|
-
:label="t('settings.providerConnection.kubernetes.insecureSkipTlsVerify')"
|
|
200
|
-
/>
|
|
201
|
-
</UFormField>
|
|
202
|
-
|
|
203
|
-
<UFormField
|
|
204
|
-
:label="
|
|
205
|
-
t('settings.providerConnection.form.optionalLabel', {
|
|
206
|
-
label: t('settings.providerConnection.kubernetes.harnessPort'),
|
|
207
|
-
})
|
|
208
|
-
"
|
|
209
|
-
>
|
|
210
|
-
<UInput v-model="form.harnessPort" type="number" class="font-mono" placeholder="8080" />
|
|
211
|
-
</UFormField>
|
|
212
|
-
|
|
213
|
-
<div v-if="supportsTest" class="flex items-center gap-2">
|
|
214
|
-
<UButton
|
|
215
|
-
color="neutral"
|
|
216
|
-
variant="soft"
|
|
217
|
-
size="sm"
|
|
218
|
-
icon="i-lucide-plug-zap"
|
|
219
|
-
:loading="testing"
|
|
220
|
-
:disabled="!canSave"
|
|
221
|
-
@click="emit('test', buildPayload())"
|
|
222
|
-
>
|
|
223
|
-
{{ t('settings.providerConnection.test.button') }}
|
|
224
|
-
</UButton>
|
|
225
|
-
<span v-if="testResult && testResult.ok" class="text-xs text-emerald-400">
|
|
226
|
-
{{ testResult.message ?? t('settings.providerConnection.test.ok') }}
|
|
227
|
-
</span>
|
|
228
|
-
<span v-else-if="testResult" class="text-xs text-rose-400">
|
|
229
|
-
{{ testResult.message ?? t('settings.providerConnection.test.failed') }}
|
|
230
|
-
</span>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
<div class="flex items-center justify-end gap-3">
|
|
234
|
-
<p v-if="connectBlockedReason" class="flex-1 text-left text-xs text-rose-400">
|
|
235
|
-
{{ connectBlockedReason }}
|
|
236
|
-
</p>
|
|
237
|
-
<UButton
|
|
238
|
-
color="primary"
|
|
239
|
-
size="sm"
|
|
240
|
-
:loading="busy"
|
|
241
|
-
:disabled="!canSave"
|
|
242
|
-
@click="emit('save', buildPayload())"
|
|
243
|
-
>
|
|
244
|
-
{{
|
|
245
|
-
connection?.kind === 'kubernetes'
|
|
246
|
-
? t('common.save')
|
|
247
|
-
: t('settings.providerConnection.form.connect')
|
|
248
|
-
}}
|
|
249
|
-
</UButton>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
</template>
|