@cat-factory/app 0.47.2 → 0.47.4
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/github/AddServiceFromRepoModal.vue +14 -12
- package/app/components/layout/IntegrationsHub.vue +44 -23
- package/app/components/settings/InfrastructureWindow.vue +221 -0
- package/app/components/settings/ProviderConnectionTab.vue +358 -0
- package/app/components/settings/ProviderManifestEditor.vue +286 -0
- package/app/pages/index.vue +3 -3
- package/app/stores/ui.ts +12 -6
- package/i18n/locales/en.json +23 -10
- package/i18n/locales/es.json +23 -10
- package/i18n/locales/fr.json +23 -10
- package/i18n/locales/pl.json +23 -10
- package/i18n/locales/uk.json +23 -10
- package/package.json +2 -1
- package/app/components/settings/ProviderConnectionPanel.vue +0 -524
|
@@ -39,18 +39,6 @@ async function loadRepos() {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// On open: ensure we know the connection + which repos the App can access, and
|
|
43
|
-
// the workspace's already-tracked repos (to flag ones already on the board).
|
|
44
|
-
watch(
|
|
45
|
-
open,
|
|
46
|
-
(isOpen) => {
|
|
47
|
-
if (!isOpen) return
|
|
48
|
-
resetSelection()
|
|
49
|
-
void loadRepos()
|
|
50
|
-
},
|
|
51
|
-
{ immediate: true },
|
|
52
|
-
)
|
|
53
|
-
|
|
54
42
|
// If the user connects from inside the modal (the not-connected prompt), pull the
|
|
55
43
|
// repo list as soon as the connection is bound.
|
|
56
44
|
watch(
|
|
@@ -152,6 +140,20 @@ const configuredBlock = computed(() =>
|
|
|
152
140
|
configuredBlockId.value ? board.getBlock(configuredBlockId.value) : undefined,
|
|
153
141
|
)
|
|
154
142
|
|
|
143
|
+
// On open: ensure we know the connection + which repos the App can access, and
|
|
144
|
+
// the workspace's already-tracked repos (to flag ones already on the board).
|
|
145
|
+
// Declared after every ref resetSelection() touches so the `immediate` run
|
|
146
|
+
// doesn't access them inside their temporal dead zone.
|
|
147
|
+
watch(
|
|
148
|
+
open,
|
|
149
|
+
(isOpen) => {
|
|
150
|
+
if (!isOpen) return
|
|
151
|
+
resetSelection()
|
|
152
|
+
void loadRepos()
|
|
153
|
+
},
|
|
154
|
+
{ immediate: true },
|
|
155
|
+
)
|
|
156
|
+
|
|
155
157
|
// A monorepo service needs a chosen directory; a whole-repo service can be added once.
|
|
156
158
|
const canAdd = computed(
|
|
157
159
|
() =>
|
|
@@ -259,32 +259,53 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
// --- Infrastructure (ephemeral environments + self-hosted runner pool) -----
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
262
|
+
// The two providers — container agents (runner pool) and Tester environments — are the
|
|
263
|
+
// same "bring your own infra" idea and the same custom pool typically backs both, so they
|
|
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.
|
|
265
269
|
const infra: IntegrationItem[] = []
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
280
299
|
infra.push({
|
|
281
|
-
key: '
|
|
300
|
+
key: 'infrastructure',
|
|
282
301
|
icon: 'i-lucide-server-cog',
|
|
283
|
-
label: t('layout.integrationsHub.items.
|
|
284
|
-
description: t('layout.integrationsHub.items.
|
|
285
|
-
status:
|
|
286
|
-
connected:
|
|
287
|
-
|
|
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)),
|
|
288
309
|
})
|
|
289
310
|
}
|
|
290
311
|
// Local-mode-only: the warm-container pool + checkout reuse for the local runner. Shown
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The single tabbed "Infrastructure" window. It merges what used to be two separate
|
|
3
|
+
// Integrations-Hub entries — the self-hosted runner pool (container agents) and the
|
|
4
|
+
// ephemeral-environment provider (Tester environments) — into one surface, because the
|
|
5
|
+
// same custom pool typically backs both jobs, so configuring them together reflects
|
|
6
|
+
// reality. Each provider gets its own tab (ProviderConnectionTab); a tab whose backend
|
|
7
|
+
// integration is disabled (503) simply doesn't render.
|
|
8
|
+
//
|
|
9
|
+
// The local-mode delegation toggles are cross-cutting (one per concern), so they live at
|
|
10
|
+
// the TOP of the window rather than buried in one tab — removing the old awkward
|
|
11
|
+
// cross-link hint that pointed from the runner-pool screen back to the env screen.
|
|
12
|
+
import { computed, ref, watch } from 'vue'
|
|
13
|
+
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
14
|
+
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
15
|
+
import ProviderConnectionTab from '~/components/settings/ProviderConnectionTab.vue'
|
|
16
|
+
|
|
17
|
+
const { t } = useI18n()
|
|
18
|
+
const ui = useUiStore()
|
|
19
|
+
const store = useProviderConnectionsStore()
|
|
20
|
+
const auth = useAuthStore()
|
|
21
|
+
const settings = useWorkspaceSettingsStore()
|
|
22
|
+
const toast = useToast()
|
|
23
|
+
|
|
24
|
+
const open = computed({
|
|
25
|
+
get: () => ui.infrastructureOpen,
|
|
26
|
+
set: (v: boolean) => {
|
|
27
|
+
if (!v) ui.closeProviderConnection()
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
const back = useIntegrationBack(open)
|
|
31
|
+
|
|
32
|
+
// Each concern gates on its own availability probe; an unavailable tab isn't offered.
|
|
33
|
+
const agentsAvailable = computed(() => store.isAvailable('runner-pool'))
|
|
34
|
+
const envsAvailable = computed(() => store.isAvailable('environment'))
|
|
35
|
+
|
|
36
|
+
const tabs = computed(() => {
|
|
37
|
+
const out: { value: ProviderConnectionKind; label: string; icon: string; slot: string }[] = []
|
|
38
|
+
if (agentsAvailable.value)
|
|
39
|
+
out.push({
|
|
40
|
+
value: 'runner-pool',
|
|
41
|
+
label: t('settings.providerConnection.tabs.containerAgents'),
|
|
42
|
+
icon: 'i-lucide-server-cog',
|
|
43
|
+
slot: 'runner-pool',
|
|
44
|
+
})
|
|
45
|
+
if (envsAvailable.value)
|
|
46
|
+
out.push({
|
|
47
|
+
value: 'environment',
|
|
48
|
+
label: t('settings.providerConnection.tabs.testEnvironments'),
|
|
49
|
+
icon: 'i-lucide-cloud',
|
|
50
|
+
slot: 'environment',
|
|
51
|
+
})
|
|
52
|
+
return out
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const activeTab = ref<ProviderConnectionKind>(ui.infrastructureTab)
|
|
56
|
+
|
|
57
|
+
// Honour the deep-linked tab each time the window opens (e.g. the banner's per-kind
|
|
58
|
+
// "Configure…" button), falling back to the first available tab if the requested one is off.
|
|
59
|
+
watch(
|
|
60
|
+
open,
|
|
61
|
+
(isOpen) => {
|
|
62
|
+
if (!isOpen) return
|
|
63
|
+
void store.ensureLoaded().catch(() => {})
|
|
64
|
+
const requested = ui.infrastructureTab
|
|
65
|
+
const available = tabs.value.map((x) => x.value)
|
|
66
|
+
activeTab.value = available.includes(requested) ? requested : (available[0] ?? requested)
|
|
67
|
+
},
|
|
68
|
+
{ immediate: true },
|
|
69
|
+
)
|
|
70
|
+
// When availability resolves after open, re-pin onto a valid tab — but keep honouring the
|
|
71
|
+
// deep-linked request. The two availability probes resolve independently, so `tabs` can pass
|
|
72
|
+
// through a transient single-tab list (e.g. the runner-pool probe lands a tick before the
|
|
73
|
+
// environment one). We must NOT let that transient list steal focus from a still-loading
|
|
74
|
+
// requested tab, so only fall back to the first tab once loading has fully settled.
|
|
75
|
+
watch([tabs, () => store.loaded], () => {
|
|
76
|
+
const list = tabs.value
|
|
77
|
+
if (list.some((x) => x.value === activeTab.value)) return
|
|
78
|
+
const requested = ui.infrastructureTab
|
|
79
|
+
if (list.some((x) => x.value === requested)) {
|
|
80
|
+
activeTab.value = requested
|
|
81
|
+
} else if (store.loaded && list.length) {
|
|
82
|
+
activeTab.value = list[0]!.value
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// --- Local-mode infrastructure delegation (cross-cutting; shown only in local mode) ---
|
|
87
|
+
// In local mode this is where a developer chooses, per workspace, whether to run on this
|
|
88
|
+
// machine (host Docker for agents, in-container docker-compose for the Tester) or delegate
|
|
89
|
+
// to an external service. Each toggle is enabled only once its provider is registered.
|
|
90
|
+
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
91
|
+
const runnerPoolRegistered = computed(() => !!store.connectionFor('runner-pool'))
|
|
92
|
+
const envRegistered = computed(() => !!store.connectionFor('environment'))
|
|
93
|
+
const savingDelegation = ref(false)
|
|
94
|
+
|
|
95
|
+
async function setDelegation(patch: {
|
|
96
|
+
delegateAgentsToRunnerPool?: boolean
|
|
97
|
+
delegateTestEnvToProvider?: boolean
|
|
98
|
+
}) {
|
|
99
|
+
savingDelegation.value = true
|
|
100
|
+
try {
|
|
101
|
+
await settings.update(patch)
|
|
102
|
+
} catch (e) {
|
|
103
|
+
toast.add({
|
|
104
|
+
title: t('settings.providerConnection.delegation.updateFailed'),
|
|
105
|
+
description: e instanceof Error ? e.message : String(e),
|
|
106
|
+
icon: 'i-lucide-triangle-alert',
|
|
107
|
+
color: 'error',
|
|
108
|
+
})
|
|
109
|
+
} finally {
|
|
110
|
+
savingDelegation.value = false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function selectTab(kind: ProviderConnectionKind) {
|
|
115
|
+
activeTab.value = kind
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<template>
|
|
120
|
+
<UModal
|
|
121
|
+
v-model:open="open"
|
|
122
|
+
:title="t('settings.providerConnection.windowTitle')"
|
|
123
|
+
:ui="{ content: 'max-w-xl' }"
|
|
124
|
+
>
|
|
125
|
+
<template #title>
|
|
126
|
+
<IntegrationBackTitle :title="t('settings.providerConnection.windowTitle')" @back="back" />
|
|
127
|
+
</template>
|
|
128
|
+
<template #body>
|
|
129
|
+
<div class="space-y-4">
|
|
130
|
+
<!-- Local-mode delegation: the local-vs-external choice for BOTH container agents
|
|
131
|
+
AND the Tester's ephemeral environments, made once here at the top. -->
|
|
132
|
+
<section
|
|
133
|
+
v-if="isLocal"
|
|
134
|
+
class="space-y-3 rounded-lg border border-slate-700 bg-slate-900/40 p-3"
|
|
135
|
+
>
|
|
136
|
+
<div>
|
|
137
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
138
|
+
{{ t('settings.providerConnection.delegation.title') }}
|
|
139
|
+
</h3>
|
|
140
|
+
<p class="mt-1 text-[11px] text-slate-400">
|
|
141
|
+
{{ t('settings.providerConnection.delegation.intro') }}
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<!-- Container agents → self-hosted runner pool -->
|
|
146
|
+
<div class="space-y-1">
|
|
147
|
+
<label class="flex items-center gap-2">
|
|
148
|
+
<USwitch
|
|
149
|
+
size="sm"
|
|
150
|
+
:model-value="settings.settings.delegateAgentsToRunnerPool"
|
|
151
|
+
:disabled="savingDelegation || !runnerPoolRegistered"
|
|
152
|
+
@update:model-value="(v) => setDelegation({ delegateAgentsToRunnerPool: v })"
|
|
153
|
+
/>
|
|
154
|
+
<span class="text-sm text-slate-200">
|
|
155
|
+
{{ t('settings.providerConnection.delegation.agentsToggle') }}
|
|
156
|
+
</span>
|
|
157
|
+
</label>
|
|
158
|
+
<p class="pl-9 text-[11px] text-slate-400">
|
|
159
|
+
{{ t('settings.providerConnection.delegation.agentsHint') }}
|
|
160
|
+
<template v-if="!runnerPoolRegistered">
|
|
161
|
+
<i18n-t
|
|
162
|
+
keypath="settings.providerConnection.delegation.registerPoolPrompt"
|
|
163
|
+
tag="span"
|
|
164
|
+
scope="global"
|
|
165
|
+
>
|
|
166
|
+
<template #link>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
class="text-sky-400 underline underline-offset-2 hover:text-sky-300"
|
|
170
|
+
@click="selectTab('runner-pool')"
|
|
171
|
+
>
|
|
172
|
+
{{ t('settings.providerConnection.delegation.registerPoolLink') }}
|
|
173
|
+
</button>
|
|
174
|
+
</template>
|
|
175
|
+
</i18n-t>
|
|
176
|
+
</template>
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Tester environments → environment provider -->
|
|
181
|
+
<div class="space-y-1">
|
|
182
|
+
<label class="flex items-center gap-2">
|
|
183
|
+
<USwitch
|
|
184
|
+
size="sm"
|
|
185
|
+
:model-value="settings.settings.delegateTestEnvToProvider"
|
|
186
|
+
:disabled="savingDelegation || !envRegistered"
|
|
187
|
+
@update:model-value="(v) => setDelegation({ delegateTestEnvToProvider: v })"
|
|
188
|
+
/>
|
|
189
|
+
<span class="text-sm text-slate-200">
|
|
190
|
+
{{ t('settings.providerConnection.delegation.envToggle') }}
|
|
191
|
+
</span>
|
|
192
|
+
</label>
|
|
193
|
+
<p class="pl-9 text-[11px] text-slate-400">
|
|
194
|
+
{{ t('settings.providerConnection.delegation.envHint') }}
|
|
195
|
+
</p>
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
|
|
199
|
+
<UTabs
|
|
200
|
+
v-if="tabs.length"
|
|
201
|
+
v-model="activeTab"
|
|
202
|
+
:items="tabs"
|
|
203
|
+
variant="link"
|
|
204
|
+
:ui="{ root: 'gap-4' }"
|
|
205
|
+
data-testid="infrastructure-tabs"
|
|
206
|
+
>
|
|
207
|
+
<template #runner-pool>
|
|
208
|
+
<ProviderConnectionTab kind="runner-pool" />
|
|
209
|
+
</template>
|
|
210
|
+
<template #environment>
|
|
211
|
+
<ProviderConnectionTab kind="environment" />
|
|
212
|
+
</template>
|
|
213
|
+
</UTabs>
|
|
214
|
+
|
|
215
|
+
<p v-else class="px-1 py-6 text-center text-sm text-slate-500">
|
|
216
|
+
{{ t('settings.providerConnection.noneAvailable') }}
|
|
217
|
+
</p>
|
|
218
|
+
</div>
|
|
219
|
+
</template>
|
|
220
|
+
</UModal>
|
|
221
|
+
</template>
|