@cat-factory/app 0.182.1 → 0.183.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/app/components/layout/IntegrationsHub.vue +6 -19
- package/app/components/settings/InfrastructureWindow.logic.spec.ts +100 -0
- package/app/components/settings/InfrastructureWindow.logic.ts +78 -0
- package/app/components/settings/InfrastructureWindow.vue +65 -46
- package/app/components/settings/PackageRegistriesPanel.vue +131 -123
- package/app/pages/index.vue +0 -1
- package/app/stores/packageRegistries.spec.ts +111 -0
- package/app/stores/packageRegistries.ts +21 -8
- package/app/stores/ui/modals.ts +20 -21
- package/app/types/providerConnections.ts +12 -0
- package/i18n/locales/de.json +5 -7
- package/i18n/locales/en.json +5 -7
- package/i18n/locales/es.json +5 -7
- package/i18n/locales/fr.json +5 -7
- package/i18n/locales/he.json +5 -7
- package/i18n/locales/it.json +5 -7
- package/i18n/locales/ja.json +5 -7
- package/i18n/locales/pl.json +5 -7
- package/i18n/locales/tr.json +5 -7
- package/i18n/locales/uk.json +5 -7
- package/package.json +2 -2
|
@@ -3,24 +3,18 @@
|
|
|
3
3
|
// orgs, GitHub Packages) that agent containers use to resolve private dependencies
|
|
4
4
|
// on checkout. Tokens are write-only: the list renders from the redacted summary
|
|
5
5
|
// (vendor + scopes + token tail) and an entry is edited by deleting + re-adding.
|
|
6
|
-
//
|
|
7
|
-
|
|
6
|
+
// Renders inline inside the Infrastructure window's "Package registries" tab: what a
|
|
7
|
+
// checkout installs from is part of where agent containers RUN, not an optional
|
|
8
|
+
// external system the workspace links in.
|
|
9
|
+
import { computed, onMounted, reactive, ref } from 'vue'
|
|
8
10
|
import type { PackageRegistryVendor } from '~/types/packageRegistries'
|
|
9
|
-
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
10
11
|
import SecretInput from '~/components/common/SecretInput.vue'
|
|
11
12
|
|
|
12
13
|
const { t } = useI18n()
|
|
13
|
-
const ui = useUiStore()
|
|
14
14
|
const store = usePackageRegistriesStore()
|
|
15
15
|
const toast = useToast()
|
|
16
16
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
17
17
|
|
|
18
|
-
const open = computed({
|
|
19
|
-
get: () => ui.packageRegistriesOpen,
|
|
20
|
-
set: (v: boolean) => (v ? ui.openPackageRegistries() : ui.closePackageRegistries()),
|
|
21
|
-
})
|
|
22
|
-
const back = useIntegrationBack(open)
|
|
23
|
-
|
|
24
18
|
// The registry vendors a workspace can connect. Fixed set — the host derives from the
|
|
25
19
|
// vendor server-side, so it renders read-only here. Vendor names stay verbatim.
|
|
26
20
|
const VENDORS: { value: PackageRegistryVendor; label: string; host: string }[] = [
|
|
@@ -59,18 +53,19 @@ function notifyError(title: string, e: unknown) {
|
|
|
59
53
|
})
|
|
60
54
|
}
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
// The tab this renders in only exists once the window's probe resolved `available === true`, so
|
|
57
|
+
// `ensureLoaded()` here would early-return every time and this error branch would be dead code.
|
|
58
|
+
// Read the list outright instead: the window owns the PROBE (a failure there means no tab), the
|
|
59
|
+
// panel owns the DATA (a failure here means the reader is looking at a list we could not fetch,
|
|
60
|
+
// and must be told). It also drops the staleness `ensureLoaded` carried — reopening the tab
|
|
61
|
+
// after an entry was added elsewhere used to re-render the first load's snapshot.
|
|
62
|
+
onMounted(async () => {
|
|
63
|
+
try {
|
|
64
|
+
await store.load()
|
|
65
|
+
} catch (e) {
|
|
66
|
+
notifyError(t('settings.packageRegistries.toast.loadFailed'), e)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
74
69
|
|
|
75
70
|
async function addEntry() {
|
|
76
71
|
busy.value = true
|
|
@@ -111,108 +106,121 @@ async function removeEntry(entryId: string) {
|
|
|
111
106
|
</script>
|
|
112
107
|
|
|
113
108
|
<template>
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<div
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</
|
|
135
|
-
<div
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<div class="flex items-center gap-2">
|
|
142
|
-
<span class="text-sm font-medium">{{ vendorLabel(entry.vendor) }}</span>
|
|
143
|
-
<span class="text-[11px] text-slate-500">
|
|
144
|
-
{{ t('settings.packageRegistries.list.tokenTail', { tail: entry.tokenTail }) }}
|
|
145
|
-
</span>
|
|
146
|
-
</div>
|
|
147
|
-
<div class="flex flex-wrap gap-1">
|
|
148
|
-
<UBadge
|
|
149
|
-
v-for="scope in entry.scopes"
|
|
150
|
-
:key="scope"
|
|
151
|
-
color="neutral"
|
|
152
|
-
variant="soft"
|
|
153
|
-
size="sm"
|
|
154
|
-
>
|
|
155
|
-
{{ scope }}
|
|
156
|
-
</UBadge>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
<UButton
|
|
160
|
-
color="error"
|
|
161
|
-
variant="ghost"
|
|
162
|
-
icon="i-lucide-trash-2"
|
|
109
|
+
<div class="space-y-4" data-testid="package-registries-panel">
|
|
110
|
+
<p class="text-sm text-slate-400">
|
|
111
|
+
{{ t('settings.packageRegistries.intro') }}
|
|
112
|
+
</p>
|
|
113
|
+
|
|
114
|
+
<section v-if="store.entries.length" class="space-y-2 rounded-lg border border-slate-700 p-3">
|
|
115
|
+
<h3 class="text-sm font-semibold">
|
|
116
|
+
{{ t('settings.packageRegistries.list.heading') }}
|
|
117
|
+
</h3>
|
|
118
|
+
<div
|
|
119
|
+
v-for="entry in store.entries"
|
|
120
|
+
:key="entry.id"
|
|
121
|
+
class="flex items-center justify-between gap-2 rounded-md border border-slate-800 px-3 py-2"
|
|
122
|
+
>
|
|
123
|
+
<div class="min-w-0 space-y-1">
|
|
124
|
+
<div class="flex items-center gap-2">
|
|
125
|
+
<span class="text-sm font-medium">{{ vendorLabel(entry.vendor) }}</span>
|
|
126
|
+
<span class="text-[11px] text-slate-500">
|
|
127
|
+
{{ t('settings.packageRegistries.list.tokenTail', { tail: entry.tokenTail }) }}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="flex flex-wrap gap-1">
|
|
131
|
+
<UBadge
|
|
132
|
+
v-for="scope in entry.scopes"
|
|
133
|
+
:key="scope"
|
|
134
|
+
color="neutral"
|
|
135
|
+
variant="soft"
|
|
163
136
|
size="sm"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
137
|
+
>
|
|
138
|
+
{{ scope }}
|
|
139
|
+
</UBadge>
|
|
140
|
+
<!-- An entry with no scopes authenticates its host without routing any scope to
|
|
141
|
+
it (the deliberate mixed public/private setup) — say so, or the row reads as
|
|
142
|
+
half-configured. -->
|
|
143
|
+
<span v-if="!entry.scopes.length" class="text-[11px] text-slate-500">
|
|
144
|
+
{{ t('settings.packageRegistries.list.noScopes') }}
|
|
145
|
+
</span>
|
|
169
146
|
</div>
|
|
170
|
-
</
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
147
|
+
</div>
|
|
148
|
+
<UButton
|
|
149
|
+
color="error"
|
|
150
|
+
variant="ghost"
|
|
151
|
+
icon="i-lucide-trash-2"
|
|
152
|
+
size="sm"
|
|
153
|
+
:loading="busy"
|
|
154
|
+
:data-testid="`package-registry-delete-${entry.id}`"
|
|
155
|
+
:aria-label="t('settings.packageRegistries.list.remove')"
|
|
156
|
+
@click="removeEntry(entry.id)"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
|
|
161
|
+
<section class="space-y-3 rounded-lg border border-slate-700 p-3">
|
|
162
|
+
<h3 class="text-sm font-semibold">
|
|
163
|
+
{{ t('settings.packageRegistries.add.heading') }}
|
|
164
|
+
</h3>
|
|
165
|
+
|
|
166
|
+
<UFormField :label="t('settings.packageRegistries.add.vendor')">
|
|
167
|
+
<USelect
|
|
168
|
+
v-model="form.vendor"
|
|
169
|
+
:items="VENDORS"
|
|
170
|
+
value-key="value"
|
|
171
|
+
class="w-full"
|
|
172
|
+
data-testid="package-registry-vendor"
|
|
173
|
+
/>
|
|
174
|
+
</UFormField>
|
|
175
|
+
<p class="text-[11px] text-slate-500">
|
|
176
|
+
{{ t('settings.packageRegistries.add.host', { host: vendorHost }) }}
|
|
177
|
+
</p>
|
|
178
|
+
|
|
179
|
+
<UFormField
|
|
180
|
+
:label="t('settings.packageRegistries.add.scopes')"
|
|
181
|
+
:help="t('settings.packageRegistries.add.scopesHelp')"
|
|
182
|
+
>
|
|
183
|
+
<UInput
|
|
184
|
+
v-model="form.scopes"
|
|
185
|
+
placeholder="@my-org, @my-other-org"
|
|
186
|
+
class="w-full"
|
|
187
|
+
data-testid="package-registry-scopes"
|
|
188
|
+
/>
|
|
189
|
+
</UFormField>
|
|
190
|
+
<!-- What will actually be SAVED. The parse splits on commas/whitespace and prefixes a
|
|
191
|
+
missing `@`, so the field's text and the stored scopes routinely differ — and now
|
|
192
|
+
that an empty list is a legitimate save, "I typed something that parsed to nothing"
|
|
193
|
+
and "I meant to leave this empty" would otherwise look identical at the button. -->
|
|
194
|
+
<div v-if="parsedScopes.length" class="flex flex-wrap gap-1">
|
|
195
|
+
<UBadge
|
|
196
|
+
v-for="scope in parsedScopes"
|
|
197
|
+
:key="scope"
|
|
198
|
+
color="neutral"
|
|
199
|
+
variant="soft"
|
|
200
|
+
size="sm"
|
|
201
|
+
data-testid="package-registry-parsed-scope"
|
|
202
|
+
>
|
|
203
|
+
{{ scope }}
|
|
204
|
+
</UBadge>
|
|
215
205
|
</div>
|
|
216
|
-
|
|
217
|
-
|
|
206
|
+
<!-- Why leaving this empty is often the RIGHT answer — a scope mapping is all-or-nothing,
|
|
207
|
+
so an org publishing some of its `@org` packages publicly breaks under one. -->
|
|
208
|
+
<p class="text-[11px] text-slate-500">
|
|
209
|
+
{{ t('settings.packageRegistries.add.scopesNote') }}
|
|
210
|
+
</p>
|
|
211
|
+
|
|
212
|
+
<UFormField :label="t('settings.packageRegistries.add.token')">
|
|
213
|
+
<SecretInput v-model="form.token" class="w-full" data-testid="package-registry-token" />
|
|
214
|
+
</UFormField>
|
|
215
|
+
|
|
216
|
+
<UButton
|
|
217
|
+
:loading="busy"
|
|
218
|
+
:disabled="!form.token.trim()"
|
|
219
|
+
data-testid="package-registry-save"
|
|
220
|
+
@click="addEntry"
|
|
221
|
+
>
|
|
222
|
+
{{ t('settings.packageRegistries.add.save') }}
|
|
223
|
+
</UButton>
|
|
224
|
+
</section>
|
|
225
|
+
</div>
|
|
218
226
|
</template>
|
package/app/pages/index.vue
CHANGED
|
@@ -437,7 +437,6 @@ watch(
|
|
|
437
437
|
<WorkspaceSettingsPanel v-if="ui.workspaceSettingsOpen" />
|
|
438
438
|
<AccountSettingsPanel v-if="ui.accountSettingsOpen" />
|
|
439
439
|
<ObservabilityConnectionPanel v-if="ui.observabilityConnectionOpen" />
|
|
440
|
-
<PackageRegistriesPanel v-if="ui.packageRegistriesOpen" />
|
|
441
440
|
<ApiTokensPanel v-if="ui.apiTokensOpen" />
|
|
442
441
|
<InfrastructureWindow v-if="ui.infrastructureOpen" />
|
|
443
442
|
<EnvironmentSetupWizard v-if="ui.environmentWizardOpen" />
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { usePackageRegistriesStore } from '~/stores/packageRegistries'
|
|
3
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
4
|
+
import type { PackageRegistryEntryView } from '~/types/packageRegistries'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The availability probe and the list read share one `load()`, but their failure contracts are
|
|
8
|
+
* opposite, and the split is what these cases pin:
|
|
9
|
+
*
|
|
10
|
+
* - a 503 is an ANSWER ("this deployment has no registries module") and resolves normally, so
|
|
11
|
+
* the Infrastructure window simply shows no tab;
|
|
12
|
+
* - anything else is a FAILURE that propagates, because the panel — which only renders once
|
|
13
|
+
* the probe already succeeded — is the surface that can tell a reader the list they are
|
|
14
|
+
* looking at could not be fetched.
|
|
15
|
+
*/
|
|
16
|
+
function entry(over: Partial<PackageRegistryEntryView> = {}): PackageRegistryEntryView {
|
|
17
|
+
return {
|
|
18
|
+
id: 'pkgreg_1',
|
|
19
|
+
ecosystem: 'npm',
|
|
20
|
+
vendor: 'npmjs',
|
|
21
|
+
scopes: ['@acme'],
|
|
22
|
+
tokenTail: 'cdef',
|
|
23
|
+
...over,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('packageRegistries store', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
useWorkspaceStore().workspaceId = 'ws1'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('load stores the entries and marks the module available', async () => {
|
|
33
|
+
vi.stubGlobal('useApi', () => ({
|
|
34
|
+
listPackageRegistries: () => Promise.resolve({ entries: [entry()] }),
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
const store = usePackageRegistriesStore()
|
|
38
|
+
await store.load()
|
|
39
|
+
|
|
40
|
+
expect(store.available).toBe(true)
|
|
41
|
+
expect(store.entries).toHaveLength(1)
|
|
42
|
+
expect(store.loading).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('a definitive 503 latches the module unavailable without throwing', async () => {
|
|
46
|
+
vi.stubGlobal('useApi', () => ({
|
|
47
|
+
listPackageRegistries: () => Promise.reject({ statusCode: 503 }),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
const store = usePackageRegistriesStore()
|
|
51
|
+
await expect(store.load()).resolves.toBeUndefined()
|
|
52
|
+
|
|
53
|
+
expect(store.available).toBe(false)
|
|
54
|
+
expect(store.entries).toEqual([])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('a transient failure propagates and leaves `available` null so the probe stays retryable', async () => {
|
|
58
|
+
vi.stubGlobal('useApi', () => ({
|
|
59
|
+
listPackageRegistries: () => Promise.reject({ statusCode: 500 }),
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
const store = usePackageRegistriesStore()
|
|
63
|
+
await expect(store.load()).rejects.toMatchObject({ statusCode: 500 })
|
|
64
|
+
|
|
65
|
+
// Never cached as a false "unavailable": a reachable-but-flaky backend must not hide a tab
|
|
66
|
+
// the deployment really has.
|
|
67
|
+
expect(store.available).toBeNull()
|
|
68
|
+
expect(store.loading).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('keeps an already-loaded list when a later refresh fails', async () => {
|
|
72
|
+
let fail = false
|
|
73
|
+
vi.stubGlobal('useApi', () => ({
|
|
74
|
+
listPackageRegistries: () =>
|
|
75
|
+
fail ? Promise.reject({ statusCode: 500 }) : Promise.resolve({ entries: [entry()] }),
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
const store = usePackageRegistriesStore()
|
|
79
|
+
await store.load()
|
|
80
|
+
fail = true
|
|
81
|
+
await expect(store.load()).rejects.toMatchObject({ statusCode: 500 })
|
|
82
|
+
|
|
83
|
+
// The panel reports the failure; it must not also blank the list the reader had.
|
|
84
|
+
expect(store.available).toBe(true)
|
|
85
|
+
expect(store.entries).toHaveLength(1)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('ensureLoaded probes once and stays retryable after a transient failure', async () => {
|
|
89
|
+
let calls = 0
|
|
90
|
+
let fail = true
|
|
91
|
+
vi.stubGlobal('useApi', () => ({
|
|
92
|
+
listPackageRegistries: () => {
|
|
93
|
+
calls += 1
|
|
94
|
+
return fail ? Promise.reject({ statusCode: 500 }) : Promise.resolve({ entries: [] })
|
|
95
|
+
},
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
const store = usePackageRegistriesStore()
|
|
99
|
+
await expect(store.ensureLoaded()).rejects.toMatchObject({ statusCode: 500 })
|
|
100
|
+
expect(calls).toBe(1)
|
|
101
|
+
|
|
102
|
+
fail = false
|
|
103
|
+
await store.ensureLoaded()
|
|
104
|
+
expect(calls).toBe(2)
|
|
105
|
+
expect(store.available).toBe(true)
|
|
106
|
+
|
|
107
|
+
// Settled now, so a third caller costs no request.
|
|
108
|
+
await store.ensureLoaded()
|
|
109
|
+
expect(calls).toBe(2)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -7,8 +7,9 @@ import { apiErrorStatus } from '~/composables/api/errors'
|
|
|
7
7
|
/**
|
|
8
8
|
* The workspace's private package-registry entries (npm private orgs, GitHub
|
|
9
9
|
* Packages) that agent containers install with. Tokens are write-only — the store
|
|
10
|
-
* only ever holds the redacted summary views. Loaded on demand (the
|
|
11
|
-
*
|
|
10
|
+
* only ever holds the redacted summary views. Loaded on demand (the Infrastructure
|
|
11
|
+
* window's "Package registries" tab, whose very existence gates on the probe below),
|
|
12
|
+
* not from the snapshot.
|
|
12
13
|
*/
|
|
13
14
|
export const usePackageRegistriesStore = defineStore('packageRegistries', () => {
|
|
14
15
|
const api = useApi()
|
|
@@ -16,8 +17,8 @@ export const usePackageRegistriesStore = defineStore('packageRegistries', () =>
|
|
|
16
17
|
const entries = ref<PackageRegistryEntryView[]>([])
|
|
17
18
|
const loading = ref(false)
|
|
18
19
|
// Mirrors the backend's opt-in gate (the module 503s when the encryption key is
|
|
19
|
-
// absent): `null` until first probed, then `true`/`false`. The
|
|
20
|
-
// registries
|
|
20
|
+
// absent): `null` until first probed, then `true`/`false`. The Infrastructure window
|
|
21
|
+
// shows no registries tab unless this is `true`.
|
|
21
22
|
const available = ref<boolean | null>(null)
|
|
22
23
|
let inFlight: Promise<void> | null = null
|
|
23
24
|
|
|
@@ -31,13 +32,25 @@ export const usePackageRegistriesStore = defineStore('packageRegistries', () =>
|
|
|
31
32
|
} catch (err) {
|
|
32
33
|
if (apiErrorStatus(err) === 503) {
|
|
33
34
|
// A definitive 503 means the integration is unconfigured (no encryption key on
|
|
34
|
-
// the backend): hide the UI entry points and stop probing.
|
|
35
|
+
// the backend): hide the UI entry points and stop probing. This is an ANSWER, not a
|
|
36
|
+
// failure, so it resolves normally.
|
|
35
37
|
available.value = false
|
|
36
38
|
entries.value = []
|
|
39
|
+
return
|
|
37
40
|
}
|
|
38
|
-
// Any other failure (transient 5xx / network)
|
|
39
|
-
// an already-available panel nor cache a false "unavailable". `available` stays
|
|
40
|
-
// `null` when never probed, so `ensureLoaded` remains retryable on the next open
|
|
41
|
+
// Any other failure (transient 5xx / network) leaves the state untouched: it must not
|
|
42
|
+
// hide an already-available panel nor cache a false "unavailable". `available` stays
|
|
43
|
+
// `null` when never probed, so `ensureLoaded` remains retryable on the next open — and
|
|
44
|
+
// the error PROPAGATES so a caller can say so. Swallowing it made every caller's error
|
|
45
|
+
// branch dead code: "the backend is unreachable" and "your deployment has no registries
|
|
46
|
+
// module" are different problems that must not render identically, and the panel is the
|
|
47
|
+
// one surface that can tell a reader which it hit. The PROBE callers still swallow (a
|
|
48
|
+
// failed probe means no tab, not a broken window) — the split is deliberate.
|
|
49
|
+
//
|
|
50
|
+
// NB `publicApiKeys` carries the same availability shape and still swallows here. It is
|
|
51
|
+
// not being changed alongside: its probe only hides one row of a hub full of others,
|
|
52
|
+
// whereas this one gates the feature's ONLY surface.
|
|
53
|
+
throw err
|
|
41
54
|
} finally {
|
|
42
55
|
loading.value = false
|
|
43
56
|
}
|
package/app/stores/ui/modals.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ref } from 'vue'
|
|
2
2
|
import type { DocumentSourceKind, InfraSetupArea, TaskSourceKind } from '~/types/domain'
|
|
3
|
+
import type { InfrastructureTab, ProviderConnectionKind } from '~/types/providerConnections'
|
|
3
4
|
import type { PendingContext } from '~/composables/useContextLinking'
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_PROVISION_DEEP_LINK_PARAM,
|
|
@@ -452,9 +453,8 @@ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
|
|
|
452
453
|
// model / agent kind, spend + activity per workspace / service / task type). Admin-gated.
|
|
453
454
|
// Distinct from `operatorDashboardOpen`, which answers the deployment-HEALTH question.
|
|
454
455
|
const reportsOpen = ref(false)
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
const packageRegistriesOpen = ref(false)
|
|
456
|
+
// NOTE: private package registries are no longer a panel of their own — they are a tab of
|
|
457
|
+
// the Infrastructure window (`infrastructureOpen`), reached from the navbar.
|
|
458
458
|
// API access tokens: the workspace's inbound public-API keys external systems present to
|
|
459
459
|
// the `/api/v1` surface. Opened from the Integrations hub.
|
|
460
460
|
const apiTokensOpen = ref(false)
|
|
@@ -505,13 +505,6 @@ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
|
|
|
505
505
|
function closeReports() {
|
|
506
506
|
reportsOpen.value = false
|
|
507
507
|
}
|
|
508
|
-
function openPackageRegistries() {
|
|
509
|
-
resetHubReturn()
|
|
510
|
-
packageRegistriesOpen.value = true
|
|
511
|
-
}
|
|
512
|
-
function closePackageRegistries() {
|
|
513
|
-
packageRegistriesOpen.value = false
|
|
514
|
-
}
|
|
515
508
|
function openApiTokens() {
|
|
516
509
|
resetHubReturn()
|
|
517
510
|
apiTokensOpen.value = true
|
|
@@ -564,7 +557,6 @@ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
|
|
|
564
557
|
observabilityConnectionOpen,
|
|
565
558
|
operatorDashboardOpen,
|
|
566
559
|
reportsOpen,
|
|
567
|
-
packageRegistriesOpen,
|
|
568
560
|
apiTokensOpen,
|
|
569
561
|
modelConfigOpen,
|
|
570
562
|
vendorCredentialsOpen,
|
|
@@ -582,8 +574,6 @@ function createIntegrationPanelModals(resetHubReturn: ResetHubReturn) {
|
|
|
582
574
|
openReports,
|
|
583
575
|
closeReports,
|
|
584
576
|
closeOperatorDashboard,
|
|
585
|
-
openPackageRegistries,
|
|
586
|
-
closePackageRegistries,
|
|
587
577
|
openApiTokens,
|
|
588
578
|
closeApiTokens,
|
|
589
579
|
openModelConfig,
|
|
@@ -683,13 +673,19 @@ function createSettingsModals(resetHubReturn: ResetHubReturn) {
|
|
|
683
673
|
*/
|
|
684
674
|
function createInfraModals(resetHubReturn: ResetHubReturn) {
|
|
685
675
|
// The single tabbed Infrastructure window — a TOP-LEVEL navbar destination (no longer
|
|
686
|
-
// reached via the Integrations hub).
|
|
687
|
-
// backend + self-hosted runner pool, plus the local-mode warm pool/checkout)
|
|
688
|
-
// environments" (the ephemeral-environment provider)
|
|
689
|
-
//
|
|
690
|
-
//
|
|
676
|
+
// reached via the Integrations hub). Its topical tabs: "Agent containers" (the execution
|
|
677
|
+
// backend + self-hosted runner pool, plus the local-mode warm pool/checkout), "Test
|
|
678
|
+
// environments" (the ephemeral-environment provider), "Shared stacks" (long-lived Compose
|
|
679
|
+
// infra an environment attaches to) and "Package registries" (the private registries a
|
|
680
|
+
// checkout installs from). `infrastructureOpen` is the modal flag; `infrastructureTab`
|
|
681
|
+
// selects the tab. `openInfrastructure()` is the navbar entry; `openProviderConnection(kind)`
|
|
682
|
+
// remains for deep-links (a banner's "Configure…" button).
|
|
683
|
+
//
|
|
684
|
+
// The ref is typed against the FULL `InfrastructureTab` union, not the provider-connection
|
|
685
|
+
// kinds: a tab this cannot name is a tab nothing can deep-link to, which is how the
|
|
686
|
+
// non-connection tabs ended up reachable only by opening the window and clicking across.
|
|
691
687
|
const infrastructureOpen = ref(false)
|
|
692
|
-
const infrastructureTab = ref<
|
|
688
|
+
const infrastructureTab = ref<InfrastructureTab>('runner-pool')
|
|
693
689
|
// Non-secret prefill captured from the `cat-factory k3s` CLI deep-link (see
|
|
694
690
|
// `consumeK3sSetupDeepLink`). When set, the Test-environments tab's kube engine form seeds the
|
|
695
691
|
// `local-k3s` connection from it; the ServiceAccount token is deliberately NOT in the link (a
|
|
@@ -704,12 +700,15 @@ function createInfraModals(resetHubReturn: ResetHubReturn) {
|
|
|
704
700
|
|
|
705
701
|
// Top-level navbar entry into the Infrastructure window. No hub-return marker (it isn't
|
|
706
702
|
// reached from the Integrations hub), so the window shows no "Back to Integrations" control.
|
|
707
|
-
function openInfrastructure(tab:
|
|
703
|
+
function openInfrastructure(tab: InfrastructureTab = 'runner-pool') {
|
|
708
704
|
resetHubReturn()
|
|
709
705
|
infrastructureTab.value = tab
|
|
710
706
|
infrastructureOpen.value = true
|
|
711
707
|
}
|
|
712
|
-
|
|
708
|
+
// Deep-link into a PROVIDER's tab specifically (a config banner's "Configure…" button), so
|
|
709
|
+
// this one stays narrowed to the connection kinds — it means "connect this provider", not
|
|
710
|
+
// "open the window somewhere". Use `openInfrastructure(tab)` for any other tab.
|
|
711
|
+
function openProviderConnection(kind: ProviderConnectionKind) {
|
|
713
712
|
resetHubReturn()
|
|
714
713
|
infrastructureTab.value = kind
|
|
715
714
|
infrastructureOpen.value = true
|
|
@@ -17,6 +17,18 @@ export type {
|
|
|
17
17
|
/** The two infrastructure providers configured through the generic connect form. */
|
|
18
18
|
export type ProviderConnectionKind = 'environment' | 'runner-pool'
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A tab of the Infrastructure window, and the vocabulary `ui.infrastructureTab` selects from.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately WIDER than {@link ProviderConnectionKind}: the window also hosts tabs that are
|
|
24
|
+
* not a provider connection at all (long-lived shared Compose stacks, the workspace's private
|
|
25
|
+
* package registries). Typing the store's tab ref as the connection kinds alone is what made
|
|
26
|
+
* those tabs unreachable by deep link — a banner, a command-palette entry or an Integrations-hub
|
|
27
|
+
* pointer could open the window but never land the user on the tab it meant. Every tab
|
|
28
|
+
* `InfrastructureWindow.vue` can render must have a name here.
|
|
29
|
+
*/
|
|
30
|
+
export type InfrastructureTab = ProviderConnectionKind | 'shared-stacks' | 'package-registries'
|
|
31
|
+
|
|
20
32
|
/** A workspace's provider binding, as exposed to clients (never secret values). */
|
|
21
33
|
export interface ProviderConnection {
|
|
22
34
|
/** The runner-backend kind for a runner-pool connection (`manifest` | `kubernetes`). */
|
package/i18n/locales/de.json
CHANGED
|
@@ -537,20 +537,22 @@
|
|
|
537
537
|
"incidentNoun": "der Incident-Provider"
|
|
538
538
|
},
|
|
539
539
|
"packageRegistries": {
|
|
540
|
-
"
|
|
540
|
+
"tab": "Package-Registries",
|
|
541
541
|
"intro": "Verbinde die privaten Registries, aus denen deine Repositories installieren. Agenten erhalten sie beim Auschecken eines Repositorys, sodass private Abhängigkeiten während der Installation aufgelöst werden. Tokens sind nur schreibbar: um eines zu ändern, entferne den Eintrag und füge ihn erneut hinzu.",
|
|
542
542
|
"entryNoun": "Registry-Eintrag",
|
|
543
543
|
"list": {
|
|
544
544
|
"heading": "Verbundene Registries",
|
|
545
545
|
"tokenTail": "Token …{tail}",
|
|
546
|
-
"remove": "Eintrag entfernen"
|
|
546
|
+
"remove": "Eintrag entfernen",
|
|
547
|
+
"noScopes": "Keine Scopes (das Token authentifiziert nur dieses Registry)"
|
|
547
548
|
},
|
|
548
549
|
"add": {
|
|
549
550
|
"heading": "Eine Registry hinzufügen",
|
|
550
551
|
"vendor": "Registry",
|
|
551
552
|
"host": "Das Token wird nur an {host} gesendet.",
|
|
552
553
|
"scopes": "Package-Scopes",
|
|
553
|
-
"scopesHelp": "Kommagetrennte npm-Scopes",
|
|
554
|
+
"scopesHelp": "Optional. Kommagetrennte npm-Scopes.",
|
|
555
|
+
"scopesNote": "Optional. Ein hier eingetragener Scope leitet jedes Paket darunter an dieses Registry, sodass ein Scope, der auch öffentlich veröffentlichte Pakete enthält, nicht mehr aufgelöst wird. Lass das Feld leer, wenn das Repository eine eigene .npmrc enthält oder wenn einzelne Abhängigkeiten über ein benanntes Registry-Präfix wie gh: festgelegt werden (pnpm 11.1 oder neuer) - das Token authentifiziert das Registry ohnehin.",
|
|
554
556
|
"token": "Zugriffstoken",
|
|
555
557
|
"save": "Registry hinzufügen"
|
|
556
558
|
},
|
|
@@ -2147,10 +2149,6 @@
|
|
|
2147
2149
|
"label": "Post-Release-Health",
|
|
2148
2150
|
"description": "Überwachen Sie Monitore und SLOs, nachdem ein Release ausgeliefert wurde (Datadog)."
|
|
2149
2151
|
},
|
|
2150
|
-
"packageRegistries": {
|
|
2151
|
-
"label": "Private Package-Registries",
|
|
2152
|
-
"description": "npm- und GitHub-Packages-Tokens, mit denen Agenten private Abhängigkeiten installieren."
|
|
2153
|
-
},
|
|
2154
2152
|
"apiTokens": {
|
|
2155
2153
|
"label": "API-Zugriffstokens",
|
|
2156
2154
|
"description": "Tokens, die externe Systeme vorlegen, um die cat-factory-API aufzurufen."
|
package/i18n/locales/en.json
CHANGED
|
@@ -2163,10 +2163,6 @@
|
|
|
2163
2163
|
"label": "Post-release health",
|
|
2164
2164
|
"description": "Watch monitors and SLOs after a release ships (Datadog)."
|
|
2165
2165
|
},
|
|
2166
|
-
"packageRegistries": {
|
|
2167
|
-
"label": "Private package registries",
|
|
2168
|
-
"description": "npm and GitHub Packages tokens agents use to install private dependencies."
|
|
2169
|
-
},
|
|
2170
2166
|
"apiTokens": {
|
|
2171
2167
|
"label": "API access tokens",
|
|
2172
2168
|
"description": "Tokens external systems present to call the cat-factory API."
|
|
@@ -2813,20 +2809,22 @@
|
|
|
2813
2809
|
"incidentNoun": "the incident provider"
|
|
2814
2810
|
},
|
|
2815
2811
|
"packageRegistries": {
|
|
2816
|
-
"
|
|
2812
|
+
"tab": "Package registries",
|
|
2817
2813
|
"intro": "Connect the private registries your repositories install from. Agents receive them when they check out a repository, so private dependencies resolve during installs. Tokens are write-only: to change one, remove the entry and add it again.",
|
|
2818
2814
|
"entryNoun": "registry entry",
|
|
2819
2815
|
"list": {
|
|
2820
2816
|
"heading": "Connected registries",
|
|
2821
2817
|
"tokenTail": "token …{tail}",
|
|
2822
|
-
"remove": "Remove entry"
|
|
2818
|
+
"remove": "Remove entry",
|
|
2819
|
+
"noScopes": "No scopes (the token authenticates this registry only)"
|
|
2823
2820
|
},
|
|
2824
2821
|
"add": {
|
|
2825
2822
|
"heading": "Add a registry",
|
|
2826
2823
|
"vendor": "Registry",
|
|
2827
2824
|
"host": "The token is sent only to {host}.",
|
|
2828
2825
|
"scopes": "Package scopes",
|
|
2829
|
-
"scopesHelp": "Comma-separated npm scopes",
|
|
2826
|
+
"scopesHelp": "Optional. Comma-separated npm scopes.",
|
|
2827
|
+
"scopesNote": "Optional. A scope listed here routes every package under it to this registry, so a scope that also holds publicly published packages stops resolving. Leave it empty when the repository commits its own .npmrc, or when single dependencies are pinned with a named-registry prefix such as gh: (pnpm 11.1 or newer) - the token authenticates the registry either way.",
|
|
2830
2828
|
"token": "Access token",
|
|
2831
2829
|
"save": "Add registry"
|
|
2832
2830
|
},
|