@cat-factory/app 0.174.0 → 0.178.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 +52 -13
- package/app/components/board/nodes/BlockNode.vue +30 -0
- package/app/components/environments/steps/EnvPickStep.vue +3 -0
- package/app/components/layout/IntegrationBackTitle.vue +11 -8
- package/app/components/layout/IntegrationsHub.vue +35 -56
- package/app/components/layout/ModelProvidersHub.vue +250 -0
- package/app/components/panels/InspectorPanel.vue +3 -28
- package/app/components/panels/inspector/ContainerSummary.vue +54 -62
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +11 -0
- package/app/components/panels/inspector/ServiceValidationConfig.vue +99 -1
- package/app/components/settings/ComposeEnvironmentSetupSection.vue +102 -0
- package/app/components/settings/InfrastructureWindow.vue +11 -1
- package/app/composables/api/validationChecks.ts +8 -0
- package/app/composables/useIntegrationBack.ts +5 -2
- package/app/composables/useNavContributions.ts +1 -1
- package/app/modular/nav-contributions.spec.ts +64 -21
- package/app/modular/nav-contributions.ts +59 -26
- package/app/pages/index.vue +4 -0
- package/app/stores/ui/modals.ts +32 -6
- package/app/stores/validationChecks.ts +17 -2
- package/app/types/validationChecks.ts +3 -0
- package/app/utils/validationDetection.spec.ts +51 -0
- package/app/utils/validationDetection.ts +67 -0
- package/i18n/locales/de.json +110 -33
- package/i18n/locales/en.json +110 -33
- package/i18n/locales/es.json +110 -33
- package/i18n/locales/fr.json +110 -33
- package/i18n/locales/he.json +110 -33
- package/i18n/locales/it.json +110 -33
- package/i18n/locales/ja.json +110 -33
- package/i18n/locales/pl.json +110 -33
- package/i18n/locales/tr.json +110 -33
- package/i18n/locales/uk.json +110 -33
- package/package.json +2 -2
|
@@ -22,72 +22,64 @@ function addTask() {
|
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
24
|
<template>
|
|
25
|
+
<!-- Modules and tasks are two peer collapsible lists, not one "Contents" group: the
|
|
26
|
+
grouping added a level of nesting without adding information, and a service's task
|
|
27
|
+
list is long enough to want collapsing independently of its modules. -->
|
|
25
28
|
<InspectorSection
|
|
26
|
-
|
|
27
|
-
:
|
|
28
|
-
:
|
|
29
|
+
v-if="modules.length"
|
|
30
|
+
:title="t('inspector.container.modulesTitle')"
|
|
31
|
+
:hint="t('inspector.container.modulesHint')"
|
|
32
|
+
:count="modules.length"
|
|
29
33
|
default-open
|
|
30
34
|
>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
)
|
|
51
|
-
}}</span>
|
|
52
|
-
</li>
|
|
53
|
-
</ul>
|
|
54
|
-
</div>
|
|
35
|
+
<ul class="space-y-1">
|
|
36
|
+
<li
|
|
37
|
+
v-for="m in modules"
|
|
38
|
+
:key="m.id"
|
|
39
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
|
|
40
|
+
@click="ui.select(m.id)"
|
|
41
|
+
>
|
|
42
|
+
<UIcon name="i-lucide-package" class="h-3.5 w-3.5 text-violet-400" />
|
|
43
|
+
<span class="truncate text-xs text-slate-200">{{ m.title }}</span>
|
|
44
|
+
<span class="ms-auto text-[10px] text-slate-500">{{
|
|
45
|
+
t(
|
|
46
|
+
'inspector.container.taskCount',
|
|
47
|
+
{ count: board.tasksOf(m.id).length },
|
|
48
|
+
board.tasksOf(m.id).length,
|
|
49
|
+
)
|
|
50
|
+
}}</span>
|
|
51
|
+
</li>
|
|
52
|
+
</ul>
|
|
53
|
+
</InspectorSection>
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class="
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<span class="ms-auto text-[10px] text-slate-500">{{
|
|
85
|
-
STATUS_META[task.status].label
|
|
86
|
-
}}</span>
|
|
87
|
-
</li>
|
|
88
|
-
</ul>
|
|
89
|
-
<div v-else class="text-[11px] text-slate-500">{{ t('inspector.container.noTasks') }}</div>
|
|
90
|
-
</div>
|
|
55
|
+
<InspectorSection
|
|
56
|
+
:title="isFrame ? t('inspector.container.allTasksTitle') : t('inspector.container.tasksTitle')"
|
|
57
|
+
:hint="t('inspector.container.tasksHint')"
|
|
58
|
+
:count="tasks.length"
|
|
59
|
+
default-open
|
|
60
|
+
>
|
|
61
|
+
<template #actions>
|
|
62
|
+
<ReviewDebtBadge />
|
|
63
|
+
<UButton size="xs" variant="soft" color="primary" icon="i-lucide-plus" @click="addTask">
|
|
64
|
+
{{ t('inspector.container.addTask') }}
|
|
65
|
+
</UButton>
|
|
66
|
+
</template>
|
|
67
|
+
<ul v-if="tasks.length" class="space-y-1">
|
|
68
|
+
<li
|
|
69
|
+
v-for="task in tasks"
|
|
70
|
+
:key="task.id"
|
|
71
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-slate-800/60"
|
|
72
|
+
@click="ui.select(task.id)"
|
|
73
|
+
>
|
|
74
|
+
<span
|
|
75
|
+
class="h-2 w-2 shrink-0 rounded-full"
|
|
76
|
+
:style="{ backgroundColor: STATUS_META[task.status].color }"
|
|
77
|
+
/>
|
|
78
|
+
<span class="truncate text-xs text-slate-200">{{ task.title }}</span>
|
|
79
|
+
<span class="ms-auto text-[10px] text-slate-500">{{ STATUS_META[task.status].label }}</span>
|
|
80
|
+
</li>
|
|
81
|
+
</ul>
|
|
82
|
+
<div v-else class="text-[11px] text-slate-500">{{ t('inspector.container.noTasks') }}</div>
|
|
91
83
|
<p v-if="isFrame" class="text-[11px] text-slate-500">
|
|
92
84
|
{{ t('inspector.container.servicesHint') }}
|
|
93
85
|
</p>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, reactive, ref, watch } from 'vue'
|
|
3
3
|
import type { Block } from '~/types/domain'
|
|
4
4
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
5
|
+
import { showOverrideField } from '~/utils/uiMode'
|
|
5
6
|
|
|
6
7
|
// Per-service (frame) post-release-health mapping: which observability monitors/SLOs the
|
|
7
8
|
// `post-release-health` gate watches after this service's PRs ship. Keyed by THIS block's
|
|
@@ -11,6 +12,7 @@ const props = defineProps<{ block: Block }>()
|
|
|
11
12
|
|
|
12
13
|
const store = useReleaseHealthStore()
|
|
13
14
|
const ui = useUiStore()
|
|
15
|
+
const uiMode = useUiModeStore()
|
|
14
16
|
const toast = useToast()
|
|
15
17
|
const { t } = useI18n()
|
|
16
18
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
@@ -21,6 +23,14 @@ const draft = reactive({ monitorIds: '', sloIds: '', envTag: '' })
|
|
|
21
23
|
const connected = computed(() => store.connection.connected)
|
|
22
24
|
const saved = computed(() => store.configForBlock(props.block.id))
|
|
23
25
|
|
|
26
|
+
// Post-release health is an ADVANCED-tier concern: with no mapping the gate is a pass-through,
|
|
27
|
+
// so basic mode hides the whole section rather than showing a service-level knob whose absence
|
|
28
|
+
// changes nothing. `showOverrideField` (not a bare `isAdvanced`) because the mapping is per
|
|
29
|
+
// SERVICE and durable — once a teammate on the advanced tier maps monitors onto this frame,
|
|
30
|
+
// hiding the panel would leave a basic-mode user with releases being watched (and an on-call
|
|
31
|
+
// agent that can spawn) by a configuration they can neither see nor clear.
|
|
32
|
+
const show = computed(() => showOverrideField(uiMode.isAdvanced, saved.value))
|
|
33
|
+
|
|
24
34
|
function parseIds(csv: string): string[] {
|
|
25
35
|
return csv
|
|
26
36
|
.split(',')
|
|
@@ -91,6 +101,7 @@ async function clear() {
|
|
|
91
101
|
|
|
92
102
|
<template>
|
|
93
103
|
<InspectorSection
|
|
104
|
+
v-if="show"
|
|
94
105
|
:title="t('inspector.releaseHealth.title')"
|
|
95
106
|
:hint="t('inspector.releaseHealth.sectionHint')"
|
|
96
107
|
>
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
VALIDATION_MAX_ATTEMPTS_CEILING,
|
|
8
8
|
VALIDATION_MAX_CHECKS,
|
|
9
9
|
type ValidationCheck,
|
|
10
|
+
type ValidationEcosystem,
|
|
10
11
|
} from '~/types/validationChecks'
|
|
12
|
+
import { mergeDetectedChecks } from '~/utils/validationDetection'
|
|
11
13
|
|
|
12
14
|
// Per-service (frame) PRE-PR VALIDATION CHECKS: the shell commands the executor-harness runs
|
|
13
15
|
// against the checkout after the coder settles and BEFORE the PR opens. A failing command's
|
|
@@ -18,10 +20,11 @@ const props = defineProps<{ block: Block }>()
|
|
|
18
20
|
|
|
19
21
|
const store = useValidationChecksStore()
|
|
20
22
|
const toast = useToast()
|
|
21
|
-
const { t } = useI18n()
|
|
23
|
+
const { t, te } = useI18n()
|
|
22
24
|
const { confirmAction, toastDone } = useConfirmAction()
|
|
23
25
|
|
|
24
26
|
const busy = ref(false)
|
|
27
|
+
const detecting = ref(false)
|
|
25
28
|
const rows = ref<ValidationCheck[]>([])
|
|
26
29
|
const maxAttempts = ref(VALIDATION_DEFAULT_MAX_ATTEMPTS)
|
|
27
30
|
|
|
@@ -64,6 +67,88 @@ function notifyError(title: string, e: unknown) {
|
|
|
64
67
|
})
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
// Ecosystem label KEYS, exhaustive over the contracts `ValidationEcosystem` union: a new
|
|
71
|
+
// backend detector fails THIS typecheck until it is mapped (the key is assembled at runtime,
|
|
72
|
+
// so the typed-message-keys check cannot see the `t()` lookup — the map's exhaustiveness is
|
|
73
|
+
// the drift guard, same pattern as `ENV_TEST_STAGE_KEYS`).
|
|
74
|
+
const ECOSYSTEM_KEYS: Record<ValidationEcosystem, string> = {
|
|
75
|
+
node: 'inspector.validationChecks.ecosystem.node',
|
|
76
|
+
python: 'inspector.validationChecks.ecosystem.python',
|
|
77
|
+
go: 'inspector.validationChecks.ecosystem.go',
|
|
78
|
+
rust: 'inspector.validationChecks.ecosystem.rust',
|
|
79
|
+
maven: 'inspector.validationChecks.ecosystem.maven',
|
|
80
|
+
gradle: 'inspector.validationChecks.ecosystem.gradle',
|
|
81
|
+
dotnet: 'inspector.validationChecks.ecosystem.dotnet',
|
|
82
|
+
ruby: 'inspector.validationChecks.ecosystem.ruby',
|
|
83
|
+
php: 'inspector.validationChecks.ecosystem.php',
|
|
84
|
+
elixir: 'inspector.validationChecks.ecosystem.elixir',
|
|
85
|
+
make: 'inspector.validationChecks.ecosystem.make',
|
|
86
|
+
just: 'inspector.validationChecks.ecosystem.just',
|
|
87
|
+
task: 'inspector.validationChecks.ecosystem.task',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ecosystemLabel(id: ValidationEcosystem): string {
|
|
91
|
+
const key = ECOSYSTEM_KEYS[id]
|
|
92
|
+
// `te`-guarded so a locale missing the key shows the raw id, never a raw message key.
|
|
93
|
+
return te(key) ? t(key) : id
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fill the rows from what the service's repo declares. The suggestion is NOT saved — it
|
|
98
|
+
* lands in the same unsaved rows the operator edits by hand, so Detect is always reversible
|
|
99
|
+
* by walking away from the panel.
|
|
100
|
+
*/
|
|
101
|
+
async function detect() {
|
|
102
|
+
detecting.value = true
|
|
103
|
+
try {
|
|
104
|
+
const result = await store.detect(props.block.id)
|
|
105
|
+
if (result.status !== 'ok') {
|
|
106
|
+
// The backend distinguishes "no repo linked" from "the repo could not be read"; say
|
|
107
|
+
// which, because they send the operator to different places.
|
|
108
|
+
toast.add({
|
|
109
|
+
title: t(`inspector.validationChecks.detect.${result.status}`),
|
|
110
|
+
icon: 'i-lucide-triangle-alert',
|
|
111
|
+
color: 'warning',
|
|
112
|
+
})
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const merged = mergeDetectedChecks(rows.value, result.checks, VALIDATION_MAX_CHECKS)
|
|
116
|
+
rows.value = merged.rows
|
|
117
|
+
if (merged.added === 0) {
|
|
118
|
+
toast.add({
|
|
119
|
+
title: t('inspector.validationChecks.detect.nothingNew'),
|
|
120
|
+
description:
|
|
121
|
+
result.checks.length > 0
|
|
122
|
+
? t('inspector.validationChecks.detect.alreadyPresent')
|
|
123
|
+
: t('inspector.validationChecks.detect.unrecognised'),
|
|
124
|
+
icon: 'i-lucide-info',
|
|
125
|
+
color: 'neutral',
|
|
126
|
+
})
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
const names = result.ecosystems.map(ecosystemLabel).join(', ')
|
|
130
|
+
toast.add({
|
|
131
|
+
title: t('inspector.validationChecks.detect.added', { count: merged.added }, merged.added),
|
|
132
|
+
// Name what was recognised AND what was left out: a cap that silently swallowed a
|
|
133
|
+
// suggestion reads as "that is everything your repo has".
|
|
134
|
+
description: [
|
|
135
|
+
names ? t('inspector.validationChecks.detect.found', { ecosystems: names }) : '',
|
|
136
|
+
merged.dropped > 0 || result.truncated
|
|
137
|
+
? t('inspector.validationChecks.detect.capped', { max: VALIDATION_MAX_CHECKS })
|
|
138
|
+
: '',
|
|
139
|
+
]
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join(' '),
|
|
142
|
+
icon: 'i-lucide-wand-sparkles',
|
|
143
|
+
color: 'success',
|
|
144
|
+
})
|
|
145
|
+
} catch (e) {
|
|
146
|
+
notifyError(t('inspector.validationChecks.detect.failed'), e)
|
|
147
|
+
} finally {
|
|
148
|
+
detecting.value = false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
67
152
|
async function save() {
|
|
68
153
|
busy.value = true
|
|
69
154
|
try {
|
|
@@ -179,6 +264,19 @@ async function clear() {
|
|
|
179
264
|
/>
|
|
180
265
|
</UFormField>
|
|
181
266
|
<div class="flex gap-2">
|
|
267
|
+
<UButton
|
|
268
|
+
color="neutral"
|
|
269
|
+
variant="soft"
|
|
270
|
+
size="xs"
|
|
271
|
+
icon="i-lucide-wand-sparkles"
|
|
272
|
+
:loading="detecting"
|
|
273
|
+
:disabled="!canAdd"
|
|
274
|
+
:title="t('inspector.validationChecks.detect.hint')"
|
|
275
|
+
data-testid="validation-detect"
|
|
276
|
+
@click="detect"
|
|
277
|
+
>
|
|
278
|
+
{{ t('inspector.validationChecks.detect.action') }}
|
|
279
|
+
</UButton>
|
|
182
280
|
<UButton
|
|
183
281
|
color="neutral"
|
|
184
282
|
variant="soft"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The guided per-service Docker Compose environment setup, as a section of the Infrastructure
|
|
3
|
+
// window's "Test environments" tab.
|
|
4
|
+
//
|
|
5
|
+
// It used to be its own top-level sidebar destination called "Environment setup", which was
|
|
6
|
+
// wrong twice over: the name said nothing about WHAT it sets up (people read it as the place
|
|
7
|
+
// to configure environments in general, which is the rest of this tab), and a wizard that
|
|
8
|
+
// fills in one field of one service's config sat at the same level as the workspace-wide
|
|
9
|
+
// infrastructure it depends on. It lives here now, next to the two settings it writes: the
|
|
10
|
+
// service's provision type and the workspace's Docker Compose handler.
|
|
11
|
+
//
|
|
12
|
+
// The section deliberately carries a full explanation rather than a one-line hint. The wizard
|
|
13
|
+
// is a five-minute flow that touches a repo and can trial-provision a stack, so "when you need
|
|
14
|
+
// this / when you don't" has to be answerable BEFORE opening it — most services on most boards
|
|
15
|
+
// never need it at all.
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
const ui = useUiStore()
|
|
18
|
+
|
|
19
|
+
/** Bullet lists rendered below; static literal keys so the typed-key check sees them. */
|
|
20
|
+
const steps = computed(() => [
|
|
21
|
+
t('settings.composeEnvSetup.how.scan'),
|
|
22
|
+
t('settings.composeEnvSetup.how.analyse'),
|
|
23
|
+
t('settings.composeEnvSetup.how.preflight'),
|
|
24
|
+
t('settings.composeEnvSetup.how.save'),
|
|
25
|
+
])
|
|
26
|
+
const needed = computed(() => [
|
|
27
|
+
t('settings.composeEnvSetup.needed.running'),
|
|
28
|
+
t('settings.composeEnvSetup.needed.compose'),
|
|
29
|
+
t('settings.composeEnvSetup.needed.setup'),
|
|
30
|
+
])
|
|
31
|
+
const notNeeded = computed(() => [
|
|
32
|
+
t('settings.composeEnvSetup.notNeeded.inContainer'),
|
|
33
|
+
t('settings.composeEnvSetup.notNeeded.otherBackend'),
|
|
34
|
+
t('settings.composeEnvSetup.notNeeded.noRepo'),
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
// The wizard is its own modal, so hand over rather than stacking: close this window first.
|
|
38
|
+
function start() {
|
|
39
|
+
ui.closeProviderConnection()
|
|
40
|
+
ui.openEnvironmentSetup()
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<section class="space-y-3" data-testid="compose-env-setup-section">
|
|
46
|
+
<div>
|
|
47
|
+
<h3 class="text-sm font-semibold text-slate-200">
|
|
48
|
+
{{ t('settings.composeEnvSetup.title') }}
|
|
49
|
+
</h3>
|
|
50
|
+
<p class="mt-1 text-xs leading-relaxed text-slate-400">
|
|
51
|
+
{{ t('settings.composeEnvSetup.lead') }}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="rounded border border-slate-800 bg-slate-900/40 p-3">
|
|
56
|
+
<p class="text-[11px] font-medium uppercase tracking-wide text-slate-400">
|
|
57
|
+
{{ t('settings.composeEnvSetup.how.title') }}
|
|
58
|
+
</p>
|
|
59
|
+
<ol class="mt-2 list-decimal space-y-1 ps-4 text-[11px] leading-relaxed text-slate-400">
|
|
60
|
+
<li v-for="(step, i) in steps" :key="`how-${i}`">{{ step }}</li>
|
|
61
|
+
</ol>
|
|
62
|
+
<p class="mt-2 text-[11px] leading-relaxed text-slate-500">
|
|
63
|
+
{{ t('settings.composeEnvSetup.how.outcome') }}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="grid gap-2 sm:grid-cols-2">
|
|
68
|
+
<div class="rounded border border-emerald-900/50 bg-emerald-950/20 p-3">
|
|
69
|
+
<p class="flex items-center gap-1.5 text-[11px] font-medium text-emerald-200/90">
|
|
70
|
+
<UIcon name="i-lucide-check" class="h-3.5 w-3.5 shrink-0" />
|
|
71
|
+
{{ t('settings.composeEnvSetup.needed.title') }}
|
|
72
|
+
</p>
|
|
73
|
+
<ul class="mt-1.5 list-disc space-y-1 ps-4 text-[11px] leading-relaxed text-slate-400">
|
|
74
|
+
<li v-for="(item, i) in needed" :key="`need-${i}`">{{ item }}</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="rounded border border-slate-800 bg-slate-900/40 p-3">
|
|
78
|
+
<p class="flex items-center gap-1.5 text-[11px] font-medium text-slate-300">
|
|
79
|
+
<UIcon name="i-lucide-minus" class="h-3.5 w-3.5 shrink-0" />
|
|
80
|
+
{{ t('settings.composeEnvSetup.notNeeded.title') }}
|
|
81
|
+
</p>
|
|
82
|
+
<ul class="mt-1.5 list-disc space-y-1 ps-4 text-[11px] leading-relaxed text-slate-400">
|
|
83
|
+
<li v-for="(item, i) in notNeeded" :key="`skip-${i}`">{{ item }}</li>
|
|
84
|
+
</ul>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="flex items-center gap-2">
|
|
89
|
+
<UButton
|
|
90
|
+
size="xs"
|
|
91
|
+
color="primary"
|
|
92
|
+
variant="soft"
|
|
93
|
+
icon="i-lucide-wand-sparkles"
|
|
94
|
+
data-testid="compose-env-setup-start"
|
|
95
|
+
@click="start()"
|
|
96
|
+
>
|
|
97
|
+
{{ t('settings.composeEnvSetup.start') }}
|
|
98
|
+
</UButton>
|
|
99
|
+
<span class="text-[11px] text-slate-500">{{ t('settings.composeEnvSetup.rerunHint') }}</span>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
</template>
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
// mode — the warm-container-pool + checkout-reuse settings (the local agent-container
|
|
7
7
|
// runtime, folded in from the former LocalModeSettingsPanel).
|
|
8
8
|
// - "Test environments" — where the Tester's ephemeral environments run. Shows the test-env
|
|
9
|
-
// backend selector
|
|
9
|
+
// backend selector, the environment-provider connection, and the guided per-service Docker
|
|
10
|
+
// Compose setup (formerly a standalone "Environment setup" sidebar entry — it writes a
|
|
11
|
+
// service's Compose recipe plus the workspace's Compose handler, so it belongs beside the
|
|
12
|
+
// settings it edits rather than at the same level as them).
|
|
10
13
|
// Local-specific affordances render inline, gated on `auth.localMode?.enabled`. A tab whose
|
|
11
14
|
// backend integration is disabled (503) simply doesn't render.
|
|
12
15
|
import { computed, ref, watch } from 'vue'
|
|
@@ -16,6 +19,7 @@ import InfraHandlersConfigurator from '~/components/settings/InfraHandlersConfig
|
|
|
16
19
|
import DefaultProvisionTypeSection from '~/components/settings/DefaultProvisionTypeSection.vue'
|
|
17
20
|
import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
|
|
18
21
|
import SharedStacksPanel from '~/components/settings/SharedStacksPanel.vue'
|
|
22
|
+
import ComposeEnvironmentSetupSection from '~/components/settings/ComposeEnvironmentSetupSection.vue'
|
|
19
23
|
|
|
20
24
|
// The shared-stacks tab uses its own slot key beyond the provider-connection kinds.
|
|
21
25
|
type InfraTabValue = ProviderConnectionKind | 'shared-stacks'
|
|
@@ -142,6 +146,12 @@ watch([tabs, () => store.loaded], () => {
|
|
|
142
146
|
<div class="border-t border-slate-800 pt-4">
|
|
143
147
|
<InfraHandlersConfigurator />
|
|
144
148
|
</div>
|
|
149
|
+
<!-- The guided per-SERVICE Compose flow (formerly the standalone "Environment
|
|
150
|
+
setup" sidebar entry). Last, because it fills in one service's recipe on
|
|
151
|
+
top of the workspace-wide choices above. -->
|
|
152
|
+
<div class="border-t border-slate-800 pt-4">
|
|
153
|
+
<ComposeEnvironmentSetupSection />
|
|
154
|
+
</div>
|
|
145
155
|
</div>
|
|
146
156
|
</template>
|
|
147
157
|
<template #shared-stacks>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deleteServiceValidationConfigContract,
|
|
3
|
+
detectServiceValidationChecksContract,
|
|
3
4
|
getServiceValidationConfigContract,
|
|
4
5
|
listServiceValidationConfigsContract,
|
|
5
6
|
setServiceValidationConfigContract,
|
|
@@ -30,6 +31,13 @@ export function validationChecksApi({ send, ws }: ApiContext) {
|
|
|
30
31
|
body,
|
|
31
32
|
}),
|
|
32
33
|
|
|
34
|
+
/** Suggest checks from the service repo's manifests — a read; the operator still saves. */
|
|
35
|
+
detectServiceValidationChecks: (workspaceId: string, blockId: string) =>
|
|
36
|
+
send(detectServiceValidationChecksContract, {
|
|
37
|
+
pathPrefix: ws(workspaceId),
|
|
38
|
+
pathParams: { blockId },
|
|
39
|
+
}),
|
|
40
|
+
|
|
33
41
|
deleteServiceValidationConfig: (workspaceId: string, blockId: string) =>
|
|
34
42
|
send(deleteServiceValidationConfigContract, {
|
|
35
43
|
pathPrefix: ws(workspaceId),
|
|
@@ -10,8 +10,9 @@ import type { Ref, WritableComputedRef } from 'vue'
|
|
|
10
10
|
* resort to an obscure comma-operator expression. A named handler reads clearly instead.
|
|
11
11
|
*
|
|
12
12
|
* Returns to whichever hub the panel was reached from: the user-scoped "My setup" hub when
|
|
13
|
-
* `cameFromPersonal` is set,
|
|
14
|
-
*
|
|
13
|
+
* `cameFromPersonal` is set, the Model providers hub when `cameFromModelProviders` is set,
|
|
14
|
+
* else the workspace Integrations hub. A shared panel (e.g. the vendor-credentials modal,
|
|
15
|
+
* reachable from all three) thus lands the user back where they were.
|
|
15
16
|
*
|
|
16
17
|
* Pass the panel's `open` model (the writable ref/computed bound to its `UModal`).
|
|
17
18
|
*/
|
|
@@ -19,8 +20,10 @@ export function useIntegrationBack(open: Ref<boolean> | WritableComputedRef<bool
|
|
|
19
20
|
const ui = useUiStore()
|
|
20
21
|
return () => {
|
|
21
22
|
const toPersonal = ui.cameFromPersonal
|
|
23
|
+
const toModelProviders = ui.cameFromModelProviders
|
|
22
24
|
open.value = false
|
|
23
25
|
if (toPersonal) ui.openPersonalSetup()
|
|
26
|
+
else if (toModelProviders) ui.openModelProviders()
|
|
24
27
|
else ui.openIntegrations()
|
|
25
28
|
}
|
|
26
29
|
}
|
|
@@ -33,10 +33,10 @@ export function useNavContributions() {
|
|
|
33
33
|
addFromRepo: () => ui.openAddService(),
|
|
34
34
|
bootstrapRepo: () => ui.openBootstrap(),
|
|
35
35
|
integrationsHub: () => ui.openIntegrations(),
|
|
36
|
+
modelProviders: () => ui.openModelProviders(),
|
|
36
37
|
sandbox: () => ui.openSandbox(),
|
|
37
38
|
kaizen: () => ui.openKaizen(),
|
|
38
39
|
infrastructure: () => ui.openInfrastructure(),
|
|
39
|
-
environmentSetup: () => ui.openEnvironmentSetup(),
|
|
40
40
|
fragmentLibrary: () => ui.openFragmentLibrary(),
|
|
41
41
|
mergeThresholds: () => ui.openWorkspaceSettings('merge'),
|
|
42
42
|
workspaceSettings: () => ui.openWorkspaceSettings(),
|
|
@@ -99,36 +99,74 @@ describe('navSlotFilter', () => {
|
|
|
99
99
|
expect(kept).toContain('integrations-hub')
|
|
100
100
|
expect(kept).toContain('workspace-settings')
|
|
101
101
|
expect(kept).toContain('model-config')
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
//
|
|
102
|
+
// Model providers are NOT an integration: the split is what makes the engines findable.
|
|
103
|
+
expect(kept).toContain('model-providers')
|
|
104
|
+
// ...and so does everything the everyday delivery loop runs on, however deep it feels:
|
|
105
|
+
// authoring a flow, the standards library, and the PREnv/runner plumbing.
|
|
105
106
|
expect(kept).toContain('build-pipeline')
|
|
106
107
|
expect(kept).toContain('fragments')
|
|
108
|
+
// Also the only route to the guided per-service Compose environment setup, which folded
|
|
109
|
+
// into this window rather than staying a sibling nav entry.
|
|
107
110
|
expect(kept).toContain('infrastructure')
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// What drops is beside the delivery path (experimentation) or a shortcut into a surface
|
|
112
|
-
// basic mode already reaches another way.
|
|
111
|
+
// What drops is either a shortcut basic mode reaches another way, or a capability
|
|
112
|
+
// deliberately kept out of the tier (experimentation, one-off repo setup, the
|
|
113
|
+
// deployment-wide operator rollups).
|
|
113
114
|
expect(kept).not.toContain('sandbox')
|
|
114
115
|
expect(kept).not.toContain('kaizen')
|
|
115
116
|
expect(kept).not.toContain('merge-thresholds')
|
|
117
|
+
expect(kept).not.toContain('bootstrap-repo')
|
|
118
|
+
expect(kept).not.toContain('operator-dashboard')
|
|
119
|
+
expect(kept).not.toContain('reports')
|
|
116
120
|
})
|
|
117
121
|
|
|
118
|
-
it('
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
'merge-thresholds':
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
it('states, per advanced item, whether basic mode still reaches its capability', () => {
|
|
123
|
+
// Marking an item `advanced` does one of two things, and which one has to be said out
|
|
124
|
+
// loud. `reached-another-way` promises a basic destination opens the same surface, so
|
|
125
|
+
// nothing is lost. `out-of-tier` concedes the opposite: the capability is ABSENT from
|
|
126
|
+
// basic mode and the tier switch is the only way to it. Both are legitimate; a silent
|
|
127
|
+
// one is not. The table is the claim, and it must match the catalog exactly, so adding
|
|
128
|
+
// `advanced: true` fails here until the reason is written down.
|
|
129
|
+
const REASON: Record<string, { kind: 'reached-another-way' | 'out-of-tier'; why: string }> = {
|
|
130
|
+
'merge-thresholds': {
|
|
131
|
+
kind: 'reached-another-way',
|
|
132
|
+
why: 'workspace-settings -> Merge tab',
|
|
133
|
+
},
|
|
134
|
+
'service-fragment-defaults': {
|
|
135
|
+
kind: 'reached-another-way',
|
|
136
|
+
why: 'workspace-settings -> Service best practices tab',
|
|
137
|
+
},
|
|
138
|
+
'local-models': {
|
|
139
|
+
kind: 'reached-another-way',
|
|
140
|
+
why: 'model-providers -> My local runners',
|
|
141
|
+
},
|
|
142
|
+
sandbox: {
|
|
143
|
+
kind: 'out-of-tier',
|
|
144
|
+
why: 'experimentation surface, beside the delivery path',
|
|
145
|
+
},
|
|
146
|
+
kaizen: {
|
|
147
|
+
kind: 'out-of-tier',
|
|
148
|
+
why: 'self-grading history, beside the delivery path',
|
|
149
|
+
},
|
|
150
|
+
'bootstrap-repo': {
|
|
151
|
+
kind: 'out-of-tier',
|
|
152
|
+
why: 'one-off new-repo setup, not part of running work on an existing board',
|
|
153
|
+
},
|
|
154
|
+
'operator-dashboard': {
|
|
155
|
+
kind: 'out-of-tier',
|
|
156
|
+
why: 'deployment-wide health rollup - an operator job, not a delivery one',
|
|
157
|
+
},
|
|
158
|
+
reports: {
|
|
159
|
+
kind: 'out-of-tier',
|
|
160
|
+
why: 'deployment-wide spend/activity rollup - an operator job, not a delivery one',
|
|
161
|
+
},
|
|
129
162
|
}
|
|
130
163
|
const advanced = NAV_CONTRIBUTIONS.filter((i) => i.advanced).map((i) => i.id)
|
|
131
|
-
expect(advanced.sort()).toEqual(Object.keys(
|
|
164
|
+
expect(advanced.sort()).toEqual(Object.keys(REASON).sort())
|
|
165
|
+
// An `out-of-tier` item is a real capability loss, so the switcher back must survive basic
|
|
166
|
+
// mode — pinned by the next case. Assert here only that every claim carries a reason.
|
|
167
|
+
for (const [id, reason] of Object.entries(REASON)) {
|
|
168
|
+
expect(reason.why.length, `${id} has no stated reason`).toBeGreaterThan(0)
|
|
169
|
+
}
|
|
132
170
|
})
|
|
133
171
|
|
|
134
172
|
it('keeps the tier switch itself reachable in basic mode', () => {
|
|
@@ -215,6 +253,7 @@ describe('NAV_CONTRIBUTIONS catalog integrity', () => {
|
|
|
215
253
|
for (const group of [
|
|
216
254
|
'create',
|
|
217
255
|
'repositories',
|
|
256
|
+
'models',
|
|
218
257
|
'integrations',
|
|
219
258
|
'infrastructure',
|
|
220
259
|
'workspaceContext',
|
|
@@ -240,15 +279,19 @@ describe('nav grouping helpers', () => {
|
|
|
240
279
|
expect(groups.map((g) => g.group)).toEqual([
|
|
241
280
|
'create',
|
|
242
281
|
'repositories',
|
|
282
|
+
'models',
|
|
243
283
|
'integrations',
|
|
244
284
|
'infrastructure',
|
|
245
285
|
'workspaceContext',
|
|
246
286
|
'configuration',
|
|
247
287
|
])
|
|
288
|
+
// The engines are their own section, ahead of the optional integrations; `model-config`
|
|
289
|
+
// sits beside the providers it picks models from rather than under `configuration`.
|
|
290
|
+
const models = groups.find((g) => g.group === 'models')
|
|
291
|
+
expect(models?.items.map((i) => i.id)).toEqual(['model-providers', 'model-config'])
|
|
248
292
|
const configuration = groups.find((g) => g.group === 'configuration')
|
|
249
293
|
expect(configuration?.items.map((i) => i.id)).toEqual([
|
|
250
294
|
'workspace-settings',
|
|
251
|
-
'model-config',
|
|
252
295
|
'account-settings',
|
|
253
296
|
'operator-dashboard',
|
|
254
297
|
'reports',
|