@cat-factory/app 0.49.2 → 0.50.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/layout/IntegrationsHub.vue +3 -66
- package/app/components/layout/SideBar.vue +39 -0
- package/app/components/panels/inspector/ServiceFragments.vue +31 -5
- package/app/components/panels/inspector/TaskStructure.vue +31 -5
- package/app/components/settings/ExecutionBackendSelector.vue +171 -0
- package/app/components/settings/InfrastructureWindow.vue +36 -126
- package/app/components/settings/LocalContainerPoolSettings.vue +181 -0
- package/app/components/settings/ProviderConnectionTab.vue +25 -12
- package/app/pages/index.vue +0 -4
- package/app/stores/auth.ts +10 -1
- package/app/stores/ui.ts +14 -18
- package/i18n/locales/en.json +24 -23
- package/i18n/locales/es.json +27 -26
- package/i18n/locales/fr.json +27 -26
- package/i18n/locales/pl.json +27 -26
- package/i18n/locales/uk.json +27 -26
- package/package.json +2 -2
- package/app/components/settings/LocalModeSettingsPanel.vue +0 -195
|
@@ -20,7 +20,6 @@ const documents = useDocumentsStore()
|
|
|
20
20
|
const tasks = useTasksStore()
|
|
21
21
|
const tracker = useTrackerStore()
|
|
22
22
|
const releaseHealth = useReleaseHealthStore()
|
|
23
|
-
const providerConnections = useProviderConnectionsStore()
|
|
24
23
|
const userSecrets = useUserSecretsStore()
|
|
25
24
|
const apiKeys = useApiKeysStore()
|
|
26
25
|
const workspace = useWorkspaceStore()
|
|
@@ -50,7 +49,6 @@ watch(
|
|
|
50
49
|
if (isOpen) {
|
|
51
50
|
query.value = ''
|
|
52
51
|
void releaseHealth.ensureLoaded().catch(() => {})
|
|
53
|
-
void providerConnections.ensureLoaded().catch(() => {})
|
|
54
52
|
void userSecrets.load().catch(() => {})
|
|
55
53
|
// Drives the OpenRouter row's "Key connected" badge.
|
|
56
54
|
if (workspace.workspaceId) void apiKeys.load(workspace.workspaceId).catch(() => {})
|
|
@@ -258,70 +256,9 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
258
256
|
})
|
|
259
257
|
}
|
|
260
258
|
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
// collapse into ONE row opening the tabbed Infrastructure window. The row shows a combined
|
|
265
|
-
// per-concern summary ("Agents: connected · Envs: not connected"). Each concern still gates
|
|
266
|
-
// on its own availability probe (a tab whose backend is off simply doesn't render), so the
|
|
267
|
-
// row appears whenever EITHER is available. The ProviderConfigBanner handles the louder
|
|
268
|
-
// "missing mandatory fields" warning.
|
|
269
|
-
const infra: IntegrationItem[] = []
|
|
270
|
-
const agentsAvailable = providerConnections.isAvailable('runner-pool')
|
|
271
|
-
const envsAvailable = providerConnections.isAvailable('environment')
|
|
272
|
-
if (agentsAvailable || envsAvailable) {
|
|
273
|
-
const agentsConn = providerConnections.connectionFor('runner-pool')
|
|
274
|
-
const envsConn = providerConnections.connectionFor('environment')
|
|
275
|
-
// Combined summary across the available concerns only.
|
|
276
|
-
const stateWord = (conn: unknown) =>
|
|
277
|
-
conn
|
|
278
|
-
? t('layout.integrationsHub.status.connected')
|
|
279
|
-
: t('layout.integrationsHub.status.notConnected')
|
|
280
|
-
const parts: string[] = []
|
|
281
|
-
if (agentsAvailable)
|
|
282
|
-
parts.push(
|
|
283
|
-
t('layout.integrationsHub.items.infrastructure.agents', { state: stateWord(agentsConn) }),
|
|
284
|
-
)
|
|
285
|
-
if (envsAvailable)
|
|
286
|
-
parts.push(
|
|
287
|
-
t('layout.integrationsHub.items.infrastructure.envs', { state: stateWord(envsConn) }),
|
|
288
|
-
)
|
|
289
|
-
// Default the window to the agents tab when available (the common case), else envs.
|
|
290
|
-
const defaultKind = agentsAvailable ? 'runner-pool' : 'environment'
|
|
291
|
-
// A concern counts as satisfied when it's unavailable (nothing to configure) or connected.
|
|
292
|
-
// The row is "connected" (green) only when EVERY available concern is connected — a
|
|
293
|
-
// half-connected state shows the amber "attention" badge instead, so the green badge can't
|
|
294
|
-
// claim "done" while one concern is still unconfigured.
|
|
295
|
-
const agentsSatisfied = !agentsAvailable || !!agentsConn
|
|
296
|
-
const envsSatisfied = !envsAvailable || !!envsConn
|
|
297
|
-
const allConnected = agentsSatisfied && envsSatisfied
|
|
298
|
-
const someConnected = !!agentsConn || !!envsConn
|
|
299
|
-
infra.push({
|
|
300
|
-
key: 'infrastructure',
|
|
301
|
-
icon: 'i-lucide-server-cog',
|
|
302
|
-
label: t('layout.integrationsHub.items.infrastructure.label'),
|
|
303
|
-
description: t('layout.integrationsHub.items.infrastructure.description'),
|
|
304
|
-
status: parts.join(' · '),
|
|
305
|
-
connected: allConnected,
|
|
306
|
-
attention: someConnected && !allConnected,
|
|
307
|
-
attentionLabel: parts.join(' · '),
|
|
308
|
-
onClick: () => go(() => ui.openProviderConnection(defaultKind)),
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
// Local-mode-only: the warm-container pool + checkout reuse for the local runner. Shown
|
|
312
|
-
// only on the local-mode service (the controller 503s elsewhere, and `auth.localMode`
|
|
313
|
-
// is set from /auth/config).
|
|
314
|
-
if (auth.localMode?.enabled) {
|
|
315
|
-
infra.push({
|
|
316
|
-
key: 'local-mode',
|
|
317
|
-
icon: 'i-lucide-container',
|
|
318
|
-
label: t('layout.integrationsHub.items.localMode.label'),
|
|
319
|
-
description: t('layout.integrationsHub.items.localMode.description'),
|
|
320
|
-
onClick: () => go(ui.openLocalModeSettings),
|
|
321
|
-
})
|
|
322
|
-
}
|
|
323
|
-
if (infra.length)
|
|
324
|
-
out.push({ title: t('layout.integrationsHub.groups.infrastructure'), items: infra })
|
|
259
|
+
// NOTE: Infrastructure (agent-container execution + Tester environments + the local-mode
|
|
260
|
+
// warm pool/checkout) is no longer listed here — it moved to its OWN top-level navbar menu
|
|
261
|
+
// (SideBar → "Infrastructure" → the tabbed Infrastructure window). See `ui.openInfrastructure`.
|
|
325
262
|
|
|
326
263
|
// --- Personal (only you) — fallback when there is no UserMenu to host "My setup" -------
|
|
327
264
|
// Per-user connections normally live in the My-setup hub; with auth disabled they fold in
|
|
@@ -20,8 +20,20 @@ const slack = useSlackStore()
|
|
|
20
20
|
const library = useFragmentLibraryStore()
|
|
21
21
|
const workspace = useWorkspaceStore()
|
|
22
22
|
const accounts = useAccountsStore()
|
|
23
|
+
const auth = useAuthStore()
|
|
24
|
+
const providerConnections = useProviderConnectionsStore()
|
|
23
25
|
const ui = useUiStore()
|
|
24
26
|
|
|
27
|
+
// The Infrastructure menu (agent-container execution + test environments) is relevant when
|
|
28
|
+
// either provider integration is available OR we're running the local-mode facade (which
|
|
29
|
+
// always has host-Docker execution + the warm-pool/checkout settings to configure).
|
|
30
|
+
const showInfrastructure = computed(
|
|
31
|
+
() =>
|
|
32
|
+
auth.localMode?.enabled === true ||
|
|
33
|
+
providerConnections.isAvailable('runner-pool') ||
|
|
34
|
+
providerConnections.isAvailable('environment'),
|
|
35
|
+
)
|
|
36
|
+
|
|
25
37
|
// `isCompact` (< lg) is the breakpoint at which the navbar is an off-canvas drawer;
|
|
26
38
|
// above it the aside is static and the drawer flag is inert.
|
|
27
39
|
const { isCompact } = useViewport()
|
|
@@ -89,6 +101,7 @@ watch(
|
|
|
89
101
|
void github.probe()
|
|
90
102
|
void slack.probe()
|
|
91
103
|
void library.probe()
|
|
104
|
+
void providerConnections.ensureLoaded().catch(() => {})
|
|
92
105
|
},
|
|
93
106
|
{ immediate: true },
|
|
94
107
|
)
|
|
@@ -241,6 +254,32 @@ watch(
|
|
|
241
254
|
</div>
|
|
242
255
|
</section>
|
|
243
256
|
|
|
257
|
+
<template v-if="showInfrastructure">
|
|
258
|
+
<USeparator />
|
|
259
|
+
<section>
|
|
260
|
+
<h2 class="mb-2 px-1 text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
261
|
+
{{ t('nav.infrastructure') }}
|
|
262
|
+
</h2>
|
|
263
|
+
<div class="space-y-1.5">
|
|
264
|
+
<!-- Where agent containers run + Tester environments + (local mode) the warm
|
|
265
|
+
pool/checkout reuse. Its own top-level destination, no longer inside the
|
|
266
|
+
Integrations hub. -->
|
|
267
|
+
<UButton
|
|
268
|
+
block
|
|
269
|
+
color="primary"
|
|
270
|
+
variant="soft"
|
|
271
|
+
size="sm"
|
|
272
|
+
icon="i-lucide-server-cog"
|
|
273
|
+
class="justify-start"
|
|
274
|
+
data-testid="nav-infrastructure"
|
|
275
|
+
@click="ui.openInfrastructure()"
|
|
276
|
+
>
|
|
277
|
+
{{ t('nav.infrastructure') }}
|
|
278
|
+
</UButton>
|
|
279
|
+
</div>
|
|
280
|
+
</section>
|
|
281
|
+
</template>
|
|
282
|
+
|
|
244
283
|
<template v-if="library.available">
|
|
245
284
|
<USeparator />
|
|
246
285
|
<section>
|
|
@@ -9,27 +9,53 @@ const props = defineProps<{ block: Block }>()
|
|
|
9
9
|
|
|
10
10
|
const board = useBoardStore()
|
|
11
11
|
const fragments = useFragmentsStore()
|
|
12
|
+
const ui = useUiStore()
|
|
13
|
+
const accounts = useAccountsStore()
|
|
12
14
|
const { t } = useI18n()
|
|
13
15
|
|
|
14
16
|
onMounted(() => fragments.ensureLoaded())
|
|
15
17
|
|
|
18
|
+
type MenuItem = { label: string; icon?: string; onSelect: () => void }
|
|
19
|
+
|
|
16
20
|
const selectedFragments = computed(() =>
|
|
17
21
|
(props.block.serviceFragmentIds ?? [])
|
|
18
22
|
.map((id) => fragments.getFragment(id))
|
|
19
23
|
.filter((f): f is NonNullable<typeof f> => !!f),
|
|
20
24
|
)
|
|
21
25
|
|
|
22
|
-
//
|
|
23
|
-
|
|
26
|
+
// A trailing group that jumps from "attach a fragment" to authoring/editing the
|
|
27
|
+
// library itself (board tier always; account tier when accounts are enabled).
|
|
28
|
+
// Open to every member — managing fragments is not an admin-only action.
|
|
29
|
+
const manageItems = computed<MenuItem[]>(() => {
|
|
30
|
+
const items: MenuItem[] = [
|
|
31
|
+
{
|
|
32
|
+
label: t('inspector.fragments.manageBoard'),
|
|
33
|
+
icon: 'i-lucide-book-marked',
|
|
34
|
+
onSelect: () => ui.openFragmentLibrary(),
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
if (accounts.enabled) {
|
|
38
|
+
items.push({
|
|
39
|
+
label: t('inspector.fragments.manageAccount'),
|
|
40
|
+
icon: 'i-lucide-users',
|
|
41
|
+
onSelect: () => ui.openAccountSettings('fragments'),
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
return items
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Picker menu: every pool fragment not already selected, grouped by category, with
|
|
48
|
+
// the management links appended as the final group.
|
|
49
|
+
const fragmentMenu = computed<MenuItem[][]>(() => {
|
|
24
50
|
const selected = new Set(props.block.serviceFragmentIds ?? [])
|
|
25
|
-
const groups = new Map<string,
|
|
51
|
+
const groups = new Map<string, MenuItem[]>()
|
|
26
52
|
for (const f of fragments.fragments) {
|
|
27
53
|
if (selected.has(f.id)) continue
|
|
28
54
|
const items = groups.get(f.category) ?? []
|
|
29
55
|
items.push({ label: f.title, onSelect: () => addFragment(f.id) })
|
|
30
56
|
groups.set(f.category, items)
|
|
31
57
|
}
|
|
32
|
-
return [...groups.values()]
|
|
58
|
+
return [...groups.values(), manageItems.value]
|
|
33
59
|
})
|
|
34
60
|
|
|
35
61
|
function addFragment(id: string) {
|
|
@@ -52,7 +78,7 @@ function removeFragment(id: string) {
|
|
|
52
78
|
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
53
79
|
{{ t('inspector.fragments.serviceTitle') }}
|
|
54
80
|
</span>
|
|
55
|
-
<UDropdownMenu
|
|
81
|
+
<UDropdownMenu :items="fragmentMenu">
|
|
56
82
|
<UButton
|
|
57
83
|
size="xs"
|
|
58
84
|
variant="ghost"
|
|
@@ -5,8 +5,12 @@ const props = defineProps<{ block: Block }>()
|
|
|
5
5
|
|
|
6
6
|
const board = useBoardStore()
|
|
7
7
|
const fragments = useFragmentsStore()
|
|
8
|
+
const ui = useUiStore()
|
|
9
|
+
const accounts = useAccountsStore()
|
|
8
10
|
const { t } = useI18n()
|
|
9
11
|
|
|
12
|
+
type MenuItem = { label: string; icon?: string; onSelect: () => void }
|
|
13
|
+
|
|
10
14
|
// ---- best-practice prompt fragments ----------------------------------------
|
|
11
15
|
// Selected fragments (resolved against the catalog; unknown ids are dropped).
|
|
12
16
|
const selectedFragments = computed(() =>
|
|
@@ -15,18 +19,40 @@ const selectedFragments = computed(() =>
|
|
|
15
19
|
.filter((f): f is NonNullable<typeof f> => !!f),
|
|
16
20
|
)
|
|
17
21
|
|
|
22
|
+
// A trailing group that jumps from "attach a fragment" to authoring/editing the
|
|
23
|
+
// library itself (board tier always; account tier when accounts are enabled).
|
|
24
|
+
// Open to every member — managing fragments is not an admin-only action.
|
|
25
|
+
const manageItems = computed<MenuItem[]>(() => {
|
|
26
|
+
const items: MenuItem[] = [
|
|
27
|
+
{
|
|
28
|
+
label: t('inspector.fragments.manageBoard'),
|
|
29
|
+
icon: 'i-lucide-book-marked',
|
|
30
|
+
onSelect: () => ui.openFragmentLibrary(),
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
if (accounts.enabled) {
|
|
34
|
+
items.push({
|
|
35
|
+
label: t('inspector.fragments.manageAccount'),
|
|
36
|
+
icon: 'i-lucide-users',
|
|
37
|
+
onSelect: () => ui.openAccountSettings('fragments'),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
return items
|
|
41
|
+
})
|
|
42
|
+
|
|
18
43
|
// Picker menu: fragments suitable for this block's type, not already selected,
|
|
19
|
-
// grouped by category so the dropdown reads like the catalog
|
|
20
|
-
|
|
44
|
+
// grouped by category so the dropdown reads like the catalog, with the management
|
|
45
|
+
// links appended as the final group.
|
|
46
|
+
const fragmentMenu = computed<MenuItem[][]>(() => {
|
|
21
47
|
const selected = new Set(props.block.fragmentIds ?? [])
|
|
22
|
-
const groups = new Map<string,
|
|
48
|
+
const groups = new Map<string, MenuItem[]>()
|
|
23
49
|
for (const f of fragments.forBlockType(props.block.type)) {
|
|
24
50
|
if (selected.has(f.id)) continue
|
|
25
51
|
const items = groups.get(f.category) ?? []
|
|
26
52
|
items.push({ label: f.title, onSelect: () => addFragment(f.id) })
|
|
27
53
|
groups.set(f.category, items)
|
|
28
54
|
}
|
|
29
|
-
return [...groups.values()]
|
|
55
|
+
return [...groups.values(), manageItems.value]
|
|
30
56
|
})
|
|
31
57
|
|
|
32
58
|
function addFragment(id: string) {
|
|
@@ -65,7 +91,7 @@ function removeFragment(id: string) {
|
|
|
65
91
|
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
66
92
|
{{ t('inspector.structure.bestPractices') }}
|
|
67
93
|
</span>
|
|
68
|
-
<UDropdownMenu
|
|
94
|
+
<UDropdownMenu :items="fragmentMenu">
|
|
69
95
|
<UButton
|
|
70
96
|
size="xs"
|
|
71
97
|
variant="ghost"
|
|
@@ -0,0 +1,171 @@
|
|
|
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>
|