@cat-factory/app 0.134.0 → 0.135.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/environments/EnvironmentSetupWizard.vue +70 -632
- package/app/components/environments/steps/EnvPickStep.vue +40 -0
- package/app/components/environments/steps/EnvPreflightStep.vue +115 -0
- package/app/components/environments/steps/EnvReviewStep.vue +381 -0
- package/app/components/environments/steps/EnvSaveStep.vue +109 -0
- package/app/components/environments/steps/JourneyStepNav.vue +30 -0
- package/app/modular/journeys/environmentSetup.frame.ts +31 -0
- package/app/modular/journeys/environmentSetup.logic.spec.ts +34 -0
- package/app/modular/journeys/environmentSetup.logic.ts +86 -0
- package/app/modular/journeys/environmentSetup.ts +143 -0
- package/app/modular/journeys/persistence.spec.ts +68 -0
- package/app/modular/journeys/persistence.ts +58 -0
- package/app/modular/registry.ts +9 -3
- package/app/plugins/modular.client.ts +37 -4
- package/app/stores/environmentWizard.ts +23 -27
- package/package.json +12 -11
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// The environment setup wizard (shared-stacks slice 7
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
2
|
+
// The environment setup wizard shell (shared-stacks slice 7; converted to a
|
|
3
|
+
// modular-vue journey in slice 3 of the modular-vue adoption —
|
|
4
|
+
// docs/initiatives/modular-vue-adoption.md).
|
|
5
|
+
//
|
|
6
|
+
// This component is now purely the MODAL + STEPPER CHROME. The step sequence,
|
|
7
|
+
// forward/back navigation, and resume are owned by the `environment-setup`
|
|
8
|
+
// journey (`~/modular/journeys/environmentSetup`), hosted here by `<JourneyHost>`:
|
|
9
|
+
// it starts the journey on open (RESUMING the in-flight instance for the same
|
|
10
|
+
// frame when one was left mid-flow, via the Pinia persistence adapter), renders
|
|
11
|
+
// the current step through `<JourneyOutlet>`, and abandons it on close. Each step
|
|
12
|
+
// component drives the `environmentWizard` store for its data/actions and fires
|
|
13
|
+
// the journey's exits to advance. On `complete` (the save step's Done) the journey
|
|
14
|
+
// clears its persisted blob and we close the modal.
|
|
15
|
+
import { computed } from 'vue'
|
|
16
|
+
import { JourneyHost, JourneyOutlet } from '@modular-vue/journeys'
|
|
17
|
+
import { environmentSetupHandle } from '~/modular/journeys/environmentSetup'
|
|
18
|
+
import { ENV_STEP_ORDER, type EnvStep } from '~/modular/journeys/environmentSetup.logic'
|
|
16
19
|
|
|
17
20
|
const ui = useUiStore()
|
|
18
|
-
const store = useEnvironmentWizardStore()
|
|
19
|
-
const preflights = usePreflightsStore()
|
|
20
21
|
const { t } = useI18n()
|
|
21
22
|
|
|
22
|
-
// The host-probe runtime isn't wired (a non-local facade 503'd) — the checklist degrades to a note.
|
|
23
|
-
const preflightsUnavailable = computed(() => preflights.available === false)
|
|
24
|
-
|
|
25
23
|
const open = computed({
|
|
26
24
|
get: () => ui.environmentWizardOpen,
|
|
27
25
|
set: (v: boolean) => {
|
|
@@ -29,118 +27,25 @@ const open = computed({
|
|
|
29
27
|
},
|
|
30
28
|
})
|
|
31
29
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
(isOpen) => {
|
|
36
|
-
if (isOpen) store.open(ui.environmentWizardFrameId)
|
|
37
|
-
},
|
|
38
|
-
{ immediate: true },
|
|
39
|
-
)
|
|
30
|
+
// Read once at journey start; keyed for resume. Frozen for the host's lifetime,
|
|
31
|
+
// so the modal remounts the host per open (it renders under `v-if` upstream).
|
|
32
|
+
const input = computed(() => ({ frameId: ui.environmentWizardFrameId }))
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
const STEP_ORDER = ['pick', 'review', 'preflight', 'save'] as const
|
|
43
|
-
type Step = (typeof STEP_ORDER)[number]
|
|
44
|
-
const STEP_LABEL = computed<Record<Step, string>>(() => ({
|
|
34
|
+
const STEP_LABEL = computed<Record<EnvStep, string>>(() => ({
|
|
45
35
|
pick: t('environmentWizard.steps.pick'),
|
|
46
36
|
review: t('environmentWizard.steps.review'),
|
|
47
37
|
preflight: t('environmentWizard.steps.preflight'),
|
|
48
38
|
save: t('environmentWizard.steps.save'),
|
|
49
39
|
}))
|
|
50
|
-
const currentIndex = computed(() => STEP_ORDER.indexOf(store.step as Step))
|
|
51
|
-
|
|
52
|
-
// ---- Provenance -------------------------------------------------------------
|
|
53
|
-
const FIELD_LABEL = computed<Record<MergeableRecipeField, string>>(() => ({
|
|
54
|
-
composeFiles: t('environmentWizard.field.composeFiles'),
|
|
55
|
-
composeProfiles: t('environmentWizard.field.composeProfiles'),
|
|
56
|
-
envFiles: t('environmentWizard.field.envFiles'),
|
|
57
|
-
externalNetworks: t('environmentWizard.field.externalNetworks'),
|
|
58
|
-
sharedStackRefs: t('environmentWizard.field.sharedStackRefs'),
|
|
59
|
-
prerequisites: t('environmentWizard.field.prerequisites'),
|
|
60
|
-
setupSteps: t('environmentWizard.field.setupSteps'),
|
|
61
|
-
healthGate: t('environmentWizard.field.healthGate'),
|
|
62
|
-
teardownSteps: t('environmentWizard.field.teardownSteps'),
|
|
63
|
-
}))
|
|
64
|
-
const ORIGIN_COLOR: Record<'detector' | 'analyst' | 'both', 'success' | 'info' | 'warning'> = {
|
|
65
|
-
detector: 'success',
|
|
66
|
-
analyst: 'info',
|
|
67
|
-
both: 'warning',
|
|
68
|
-
}
|
|
69
|
-
const ORIGIN_LABEL = computed<Record<'detector' | 'analyst' | 'both', string>>(() => ({
|
|
70
|
-
detector: t('environmentWizard.origin.detector'),
|
|
71
|
-
analyst: t('environmentWizard.origin.analyst'),
|
|
72
|
-
both: t('environmentWizard.origin.both'),
|
|
73
|
-
}))
|
|
74
|
-
|
|
75
|
-
// ---- Candidate helpers ------------------------------------------------------
|
|
76
|
-
const composeFileCandidates = computed<ProvisioningComposeFileCandidate[]>(
|
|
77
|
-
() => store.recommendation?.composeFileCandidates ?? [],
|
|
78
|
-
)
|
|
79
|
-
const profileCandidates = computed<ProvisioningProfileCandidate[]>(
|
|
80
|
-
() => store.recommendation?.profileCandidates ?? [],
|
|
81
|
-
)
|
|
82
|
-
const seedDumpCandidates = computed<ProvisioningSeedDumpCandidate[]>(
|
|
83
|
-
() => store.recommendation?.seedDumpCandidates ?? [],
|
|
84
|
-
)
|
|
85
|
-
const composeServiceOptions = computed(() =>
|
|
86
|
-
(store.recommendation?.composeServiceCandidates ?? []).map((c) => ({
|
|
87
|
-
label: c.recommended ? `${c.service} · ${t('environmentWizard.recommended')}` : c.service,
|
|
88
|
-
value: c.service,
|
|
89
|
-
})),
|
|
90
|
-
)
|
|
91
|
-
// The repo ships its own imperative bring-up (a `bin/*console*` / Makefile / justfile the
|
|
92
|
-
// deterministic scan can't read) — the strongest signal to run the analyst, so when present we
|
|
93
|
-
// elevate the deep-analysis affordance from a quiet opt-in to a prominent nudge naming the CLI.
|
|
94
|
-
const repoCliHint = computed(() => store.recommendation?.repoCliHint)
|
|
95
|
-
|
|
96
|
-
function fileEnabled(path: string): boolean {
|
|
97
|
-
return store.recipe.composeFiles?.includes(path) ?? false
|
|
98
|
-
}
|
|
99
|
-
function profileEnabled(profile: string): boolean {
|
|
100
|
-
return store.recipe.composeProfiles?.includes(profile) ?? false
|
|
101
|
-
}
|
|
102
|
-
function seedAdded(path: string): boolean {
|
|
103
|
-
return (store.recipe.setupSteps ?? []).some(
|
|
104
|
-
(s) => s.kind === 'compose-exec' && s.stdinFile === path,
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ---- Raw recipe editor ------------------------------------------------------
|
|
109
|
-
const rawOpen = ref(false)
|
|
110
|
-
const rawText = ref('')
|
|
111
|
-
const rawError = ref<string | null>(null)
|
|
112
|
-
function openRaw() {
|
|
113
|
-
rawText.value = JSON.stringify(store.recipe, null, 2)
|
|
114
|
-
rawError.value = null
|
|
115
|
-
rawOpen.value = true
|
|
116
|
-
}
|
|
117
|
-
function toggleRaw() {
|
|
118
|
-
if (rawOpen.value) rawOpen.value = false
|
|
119
|
-
else openRaw()
|
|
120
|
-
}
|
|
121
|
-
function applyRaw() {
|
|
122
|
-
rawError.value = store.setRecipeFromJson(rawText.value)
|
|
123
|
-
if (!rawError.value) rawOpen.value = false
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---- Preflight verdict presentation -----------------------------------------
|
|
127
|
-
const PREFLIGHT_COLOR: Record<'pass' | 'warn' | 'fail', 'success' | 'warning' | 'error'> = {
|
|
128
|
-
pass: 'success',
|
|
129
|
-
warn: 'warning',
|
|
130
|
-
fail: 'error',
|
|
131
|
-
}
|
|
132
40
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
function next() {
|
|
142
|
-
const idx = currentIndex.value
|
|
143
|
-
if (idx < STEP_ORDER.length - 1) store.goToStep(STEP_ORDER[idx + 1]!)
|
|
41
|
+
/** The crumb index of a step, relative to the current journey step. */
|
|
42
|
+
function crumbState(
|
|
43
|
+
entry: EnvStep,
|
|
44
|
+
currentEntry: EnvStep | undefined,
|
|
45
|
+
): 'current' | 'done' | 'todo' {
|
|
46
|
+
if (!currentEntry) return 'todo'
|
|
47
|
+
if (entry === currentEntry) return 'current'
|
|
48
|
+
return ENV_STEP_ORDER.indexOf(entry) < ENV_STEP_ORDER.indexOf(currentEntry) ? 'done' : 'todo'
|
|
144
49
|
}
|
|
145
50
|
</script>
|
|
146
51
|
|
|
@@ -153,522 +58,55 @@ function next() {
|
|
|
153
58
|
>
|
|
154
59
|
<template #body>
|
|
155
60
|
<div class="space-y-5" data-testid="env-setup-wizard">
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
:key="s"
|
|
161
|
-
class="flex items-center gap-2"
|
|
162
|
-
:data-testid="`env-setup-crumb-${s}`"
|
|
163
|
-
>
|
|
164
|
-
<span
|
|
165
|
-
class="flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold"
|
|
166
|
-
:class="
|
|
167
|
-
i === currentIndex
|
|
168
|
-
? 'bg-primary-500 text-white'
|
|
169
|
-
: i < currentIndex
|
|
170
|
-
? 'bg-emerald-600/70 text-white'
|
|
171
|
-
: 'bg-slate-700 text-slate-300'
|
|
172
|
-
"
|
|
173
|
-
>{{ i + 1 }}</span
|
|
174
|
-
>
|
|
175
|
-
<span :class="i === currentIndex ? 'text-slate-100' : 'text-slate-500'">
|
|
176
|
-
{{ STEP_LABEL[s] }}
|
|
177
|
-
</span>
|
|
178
|
-
<UIcon
|
|
179
|
-
v-if="i < STEP_ORDER.length - 1"
|
|
180
|
-
name="i-lucide-chevron-right"
|
|
181
|
-
class="h-3 w-3 text-slate-600"
|
|
182
|
-
/>
|
|
183
|
-
</li>
|
|
184
|
-
</ol>
|
|
185
|
-
|
|
186
|
-
<!-- ============================ PICK ============================ -->
|
|
187
|
-
<section v-if="store.step === 'pick'" class="space-y-3" data-testid="env-setup-step-pick">
|
|
188
|
-
<p class="text-sm text-slate-400">{{ t('environmentWizard.pick.intro') }}</p>
|
|
189
|
-
<p v-if="!store.serviceFrames.length" class="text-sm text-slate-500">
|
|
190
|
-
{{ t('environmentWizard.pick.empty') }}
|
|
191
|
-
</p>
|
|
192
|
-
<div v-else class="space-y-1.5">
|
|
193
|
-
<UButton
|
|
194
|
-
v-for="frame in store.serviceFrames"
|
|
195
|
-
:key="frame.id"
|
|
196
|
-
block
|
|
197
|
-
color="neutral"
|
|
198
|
-
variant="soft"
|
|
199
|
-
class="justify-start"
|
|
200
|
-
icon="i-lucide-box"
|
|
201
|
-
:data-testid="`env-setup-frame-${frame.id}`"
|
|
202
|
-
@click="store.selectFrame(frame.id)"
|
|
203
|
-
>
|
|
204
|
-
{{ frame.title }}
|
|
205
|
-
</UButton>
|
|
206
|
-
</div>
|
|
207
|
-
</section>
|
|
208
|
-
|
|
209
|
-
<!-- =========================== REVIEW =========================== -->
|
|
210
|
-
<section
|
|
211
|
-
v-else-if="store.step === 'review'"
|
|
212
|
-
class="space-y-4"
|
|
213
|
-
data-testid="env-setup-step-review"
|
|
61
|
+
<JourneyHost
|
|
62
|
+
:handle="environmentSetupHandle"
|
|
63
|
+
:input="input"
|
|
64
|
+
@finished="ui.closeEnvironmentSetup()"
|
|
214
65
|
>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<UButton
|
|
224
|
-
size="xs"
|
|
225
|
-
variant="soft"
|
|
226
|
-
color="primary"
|
|
227
|
-
icon="i-lucide-wand-sparkles"
|
|
228
|
-
:loading="store.detecting"
|
|
229
|
-
:disabled="!store.hasRepo"
|
|
230
|
-
data-testid="env-setup-detect"
|
|
231
|
-
@click="store.detect()"
|
|
232
|
-
>
|
|
233
|
-
{{ t('environmentWizard.review.detect') }}
|
|
234
|
-
</UButton>
|
|
235
|
-
</div>
|
|
236
|
-
|
|
237
|
-
<p v-if="!store.hasRepo" class="text-[12px] text-amber-300/80">
|
|
238
|
-
{{ t('environmentWizard.review.noRepo') }}
|
|
239
|
-
</p>
|
|
240
|
-
<p
|
|
241
|
-
v-else-if="store.detectError"
|
|
242
|
-
class="text-[12px] text-rose-300/80"
|
|
243
|
-
data-testid="env-setup-detect-error"
|
|
244
|
-
>
|
|
245
|
-
{{ t('environmentWizard.review.detectError') }}
|
|
246
|
-
</p>
|
|
247
|
-
|
|
248
|
-
<template v-if="store.recommendation && !store.detecting">
|
|
249
|
-
<!-- deep analysis (opt-in; elevated to a prominent nudge when the repo ships its own CLI) -->
|
|
250
|
-
<div
|
|
251
|
-
class="rounded-md border p-3"
|
|
252
|
-
:class="
|
|
253
|
-
repoCliHint
|
|
254
|
-
? 'border-primary-700/60 bg-primary-950/30'
|
|
255
|
-
: 'border-slate-800 bg-slate-900/40'
|
|
256
|
-
"
|
|
257
|
-
>
|
|
258
|
-
<p
|
|
259
|
-
v-if="repoCliHint"
|
|
260
|
-
class="mb-2 flex items-start gap-1.5 text-[11px] text-primary-300"
|
|
261
|
-
data-testid="env-setup-cli-nudge"
|
|
262
|
-
>
|
|
263
|
-
<UIcon name="i-lucide-lightbulb" class="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
264
|
-
<span>{{
|
|
265
|
-
t('environmentWizard.analysis.cliNudge', { path: repoCliHint.path })
|
|
266
|
-
}}</span>
|
|
267
|
-
</p>
|
|
268
|
-
<div class="flex items-center justify-between gap-2">
|
|
269
|
-
<div class="min-w-0">
|
|
270
|
-
<p class="text-[12px] font-medium text-slate-300">
|
|
271
|
-
{{ t('environmentWizard.analysis.title') }}
|
|
272
|
-
</p>
|
|
273
|
-
<p class="text-[11px] text-slate-500">
|
|
274
|
-
{{ t('environmentWizard.analysis.hint') }}
|
|
275
|
-
</p>
|
|
276
|
-
</div>
|
|
277
|
-
<UButton
|
|
278
|
-
size="xs"
|
|
279
|
-
variant="soft"
|
|
280
|
-
:color="repoCliHint ? 'primary' : 'neutral'"
|
|
281
|
-
icon="i-lucide-sparkles"
|
|
282
|
-
:loading="store.analysisStatus === 'running'"
|
|
283
|
-
:disabled="!store.canAnalyze || store.analysisStatus === 'running'"
|
|
284
|
-
data-testid="env-setup-run-analysis"
|
|
285
|
-
@click="store.startAnalysis()"
|
|
286
|
-
>
|
|
287
|
-
{{ t('environmentWizard.analysis.run') }}
|
|
288
|
-
</UButton>
|
|
289
|
-
</div>
|
|
290
|
-
<p
|
|
291
|
-
v-if="!store.canAnalyze"
|
|
292
|
-
class="mt-2 text-[11px] text-slate-500"
|
|
293
|
-
data-testid="env-setup-analysis-unavailable"
|
|
294
|
-
>
|
|
295
|
-
{{ t('environmentWizard.analysis.unavailable') }}
|
|
296
|
-
</p>
|
|
297
|
-
<p
|
|
298
|
-
v-else-if="store.analysisStatus === 'failed'"
|
|
299
|
-
class="mt-2 text-[11px] text-rose-300/80"
|
|
66
|
+
<template #default="{ instanceId, instance }">
|
|
67
|
+
<!-- stepper header, driven by the journey's current step -->
|
|
68
|
+
<ol class="flex items-center gap-2 text-[11px]">
|
|
69
|
+
<li
|
|
70
|
+
v-for="(s, i) in ENV_STEP_ORDER"
|
|
71
|
+
:key="s"
|
|
72
|
+
class="flex items-center gap-2"
|
|
73
|
+
:data-testid="`env-setup-crumb-${s}`"
|
|
300
74
|
>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
size="xs"
|
|
313
|
-
variant="soft"
|
|
314
|
-
color="primary"
|
|
315
|
-
icon="i-lucide-git-merge"
|
|
316
|
-
data-testid="env-setup-apply-analysis"
|
|
317
|
-
@click="store.applyAnalystDraft()"
|
|
318
|
-
>
|
|
319
|
-
{{ t('environmentWizard.analysis.apply') }}
|
|
320
|
-
</UButton>
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
|
|
324
|
-
<!-- per-field provenance -->
|
|
325
|
-
<div v-if="store.merged?.fields.length" class="space-y-1.5">
|
|
326
|
-
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
327
|
-
{{ t('environmentWizard.review.provenanceTitle') }}
|
|
328
|
-
</p>
|
|
329
|
-
<div class="flex flex-wrap gap-1.5">
|
|
330
|
-
<UBadge
|
|
331
|
-
v-for="f in store.merged.fields"
|
|
332
|
-
:key="f.field"
|
|
333
|
-
:color="ORIGIN_COLOR[f.origin]"
|
|
334
|
-
variant="subtle"
|
|
335
|
-
size="sm"
|
|
336
|
-
:title="f.detectorMessage ?? f.analystRationale ?? ''"
|
|
337
|
-
:data-testid="`env-setup-field-${f.field}`"
|
|
338
|
-
>
|
|
339
|
-
{{ FIELD_LABEL[f.field] }} · {{ ORIGIN_LABEL[f.origin] }}
|
|
340
|
-
</UBadge>
|
|
341
|
-
</div>
|
|
342
|
-
</div>
|
|
343
|
-
|
|
344
|
-
<!-- exposed compose service + port -->
|
|
345
|
-
<div class="grid grid-cols-2 gap-3">
|
|
346
|
-
<UFormField :label="t('environmentWizard.review.service')" required>
|
|
347
|
-
<USelect
|
|
348
|
-
v-if="composeServiceOptions.length"
|
|
349
|
-
v-model="store.composeService"
|
|
350
|
-
:items="composeServiceOptions"
|
|
351
|
-
value-key="value"
|
|
352
|
-
:placeholder="t('environmentWizard.review.servicePlaceholder')"
|
|
353
|
-
class="w-full"
|
|
354
|
-
data-testid="env-setup-service-select"
|
|
355
|
-
/>
|
|
356
|
-
<UInput
|
|
357
|
-
v-else
|
|
358
|
-
v-model="store.composeService"
|
|
359
|
-
:placeholder="t('environmentWizard.review.servicePlaceholder')"
|
|
360
|
-
class="w-full"
|
|
361
|
-
data-testid="env-setup-service-input"
|
|
362
|
-
/>
|
|
363
|
-
</UFormField>
|
|
364
|
-
<UFormField :label="t('environmentWizard.review.port')">
|
|
365
|
-
<UInput
|
|
366
|
-
v-model.number="store.exposedPort"
|
|
367
|
-
type="number"
|
|
368
|
-
class="w-full"
|
|
369
|
-
data-testid="env-setup-port"
|
|
370
|
-
/>
|
|
371
|
-
</UFormField>
|
|
372
|
-
</div>
|
|
373
|
-
|
|
374
|
-
<!-- compose file layering -->
|
|
375
|
-
<div v-if="composeFileCandidates.length" class="space-y-1.5">
|
|
376
|
-
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
377
|
-
{{ t('environmentWizard.review.composeFiles') }}
|
|
378
|
-
</p>
|
|
379
|
-
<div class="flex flex-wrap gap-1.5">
|
|
380
|
-
<UButton
|
|
381
|
-
v-for="c in composeFileCandidates"
|
|
382
|
-
:key="c.path"
|
|
383
|
-
size="xs"
|
|
384
|
-
:color="fileEnabled(c.path) ? 'primary' : 'neutral'"
|
|
385
|
-
:variant="fileEnabled(c.path) ? 'soft' : 'ghost'"
|
|
386
|
-
:icon="fileEnabled(c.path) ? 'i-lucide-check' : 'i-lucide-plus'"
|
|
387
|
-
:data-testid="`env-setup-file-${c.name}`"
|
|
388
|
-
@click="store.toggleComposeFile(c.path)"
|
|
389
|
-
>
|
|
390
|
-
{{ c.name }}<span v-if="c.os" class="ms-1 text-[9px] opacity-70">{{ c.os }}</span>
|
|
391
|
-
</UButton>
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
|
-
<!-- compose profiles -->
|
|
396
|
-
<div v-if="profileCandidates.length" class="space-y-1.5">
|
|
397
|
-
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
398
|
-
{{ t('environmentWizard.review.profiles') }}
|
|
399
|
-
</p>
|
|
400
|
-
<div class="flex flex-wrap gap-1.5">
|
|
401
|
-
<UButton
|
|
402
|
-
v-for="c in profileCandidates"
|
|
403
|
-
:key="c.profile"
|
|
404
|
-
size="xs"
|
|
405
|
-
:color="profileEnabled(c.profile) ? 'primary' : 'neutral'"
|
|
406
|
-
:variant="profileEnabled(c.profile) ? 'soft' : 'ghost'"
|
|
407
|
-
:icon="profileEnabled(c.profile) ? 'i-lucide-check' : 'i-lucide-plus'"
|
|
408
|
-
:data-testid="`env-setup-profile-${c.profile}`"
|
|
409
|
-
@click="store.toggleProfile(c.profile)"
|
|
410
|
-
>
|
|
411
|
-
{{ c.profile }}
|
|
412
|
-
</UButton>
|
|
413
|
-
</div>
|
|
414
|
-
</div>
|
|
415
|
-
|
|
416
|
-
<!-- seed dumps -->
|
|
417
|
-
<div v-if="seedDumpCandidates.length" class="space-y-1.5">
|
|
418
|
-
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
419
|
-
{{ t('environmentWizard.review.seedDumps') }}
|
|
420
|
-
</p>
|
|
421
|
-
<div class="space-y-1">
|
|
422
|
-
<div
|
|
423
|
-
v-for="c in seedDumpCandidates"
|
|
424
|
-
:key="c.path"
|
|
425
|
-
class="flex items-center justify-between gap-2 rounded border border-slate-800 bg-slate-900/40 px-2 py-1"
|
|
75
|
+
<span
|
|
76
|
+
class="flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold"
|
|
77
|
+
:class="{
|
|
78
|
+
'bg-primary-500 text-white':
|
|
79
|
+
crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'current',
|
|
80
|
+
'bg-emerald-600/70 text-white':
|
|
81
|
+
crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'done',
|
|
82
|
+
'bg-slate-700 text-slate-300':
|
|
83
|
+
crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'todo',
|
|
84
|
+
}"
|
|
85
|
+
>{{ i + 1 }}</span
|
|
426
86
|
>
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
:disabled="seedAdded(c.path)"
|
|
434
|
-
:data-testid="`env-setup-seed-${c.name}`"
|
|
435
|
-
@click="store.addSeedStep(c)"
|
|
436
|
-
>
|
|
437
|
-
{{
|
|
438
|
-
seedAdded(c.path)
|
|
439
|
-
? t('environmentWizard.review.seedAdded')
|
|
440
|
-
: t('environmentWizard.review.seedAdd')
|
|
441
|
-
}}
|
|
442
|
-
</UButton>
|
|
443
|
-
</div>
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
|
|
447
|
-
<!-- raw recipe editor -->
|
|
448
|
-
<div>
|
|
449
|
-
<UButton
|
|
450
|
-
size="xs"
|
|
451
|
-
variant="ghost"
|
|
452
|
-
color="neutral"
|
|
453
|
-
:icon="rawOpen ? 'i-lucide-chevron-down' : 'i-lucide-code'"
|
|
454
|
-
data-testid="env-setup-raw-toggle"
|
|
455
|
-
@click="toggleRaw"
|
|
456
|
-
>
|
|
457
|
-
{{ t('environmentWizard.review.rawEditor') }}
|
|
458
|
-
</UButton>
|
|
459
|
-
<div v-if="rawOpen" class="mt-2 space-y-2">
|
|
460
|
-
<UTextarea
|
|
461
|
-
v-model="rawText"
|
|
462
|
-
:rows="10"
|
|
463
|
-
class="w-full font-mono text-[11px]"
|
|
464
|
-
data-testid="env-setup-raw-text"
|
|
465
|
-
/>
|
|
466
|
-
<p
|
|
467
|
-
v-if="rawError"
|
|
468
|
-
class="text-[11px] text-rose-300/80"
|
|
469
|
-
data-testid="env-setup-raw-error"
|
|
87
|
+
<span
|
|
88
|
+
:class="
|
|
89
|
+
crumbState(s, (instance?.step?.entry as EnvStep) ?? undefined) === 'current'
|
|
90
|
+
? 'text-slate-100'
|
|
91
|
+
: 'text-slate-500'
|
|
92
|
+
"
|
|
470
93
|
>
|
|
471
|
-
{{
|
|
472
|
-
</p>
|
|
473
|
-
<div class="flex justify-end">
|
|
474
|
-
<UButton
|
|
475
|
-
size="xs"
|
|
476
|
-
color="primary"
|
|
477
|
-
data-testid="env-setup-raw-apply"
|
|
478
|
-
@click="applyRaw"
|
|
479
|
-
>
|
|
480
|
-
{{ t('environmentWizard.review.rawApply') }}
|
|
481
|
-
</UButton>
|
|
482
|
-
</div>
|
|
483
|
-
</div>
|
|
484
|
-
</div>
|
|
485
|
-
</template>
|
|
486
|
-
</section>
|
|
487
|
-
|
|
488
|
-
<!-- ========================== PREFLIGHT ========================= -->
|
|
489
|
-
<section
|
|
490
|
-
v-else-if="store.step === 'preflight'"
|
|
491
|
-
class="space-y-3"
|
|
492
|
-
data-testid="env-setup-step-preflight"
|
|
493
|
-
>
|
|
494
|
-
<div class="flex items-center justify-between gap-2">
|
|
495
|
-
<p class="text-sm text-slate-400">{{ t('environmentWizard.preflight.intro') }}</p>
|
|
496
|
-
<UButton
|
|
497
|
-
size="xs"
|
|
498
|
-
variant="soft"
|
|
499
|
-
color="primary"
|
|
500
|
-
icon="i-lucide-list-checks"
|
|
501
|
-
:loading="store.preflightRunning"
|
|
502
|
-
data-testid="env-setup-preflight-run"
|
|
503
|
-
@click="store.runPreflight()"
|
|
504
|
-
>
|
|
505
|
-
{{ t('environmentWizard.preflight.run') }}
|
|
506
|
-
</UButton>
|
|
507
|
-
</div>
|
|
508
|
-
|
|
509
|
-
<p
|
|
510
|
-
v-if="!store.recipe.prerequisites?.length"
|
|
511
|
-
class="text-[12px] text-slate-500"
|
|
512
|
-
data-testid="env-setup-preflight-none"
|
|
513
|
-
>
|
|
514
|
-
{{ t('environmentWizard.preflight.none') }}
|
|
515
|
-
</p>
|
|
516
|
-
<p
|
|
517
|
-
v-else-if="preflightsUnavailable"
|
|
518
|
-
class="text-[12px] text-amber-300/80"
|
|
519
|
-
data-testid="env-setup-preflight-unavailable"
|
|
520
|
-
>
|
|
521
|
-
{{ t('environmentWizard.preflight.unavailable') }}
|
|
522
|
-
</p>
|
|
523
|
-
<p
|
|
524
|
-
v-if="store.preflightError"
|
|
525
|
-
class="text-[12px] text-rose-300/80"
|
|
526
|
-
data-testid="env-setup-preflight-error"
|
|
527
|
-
>
|
|
528
|
-
{{ store.preflightError }}
|
|
529
|
-
</p>
|
|
530
|
-
|
|
531
|
-
<ul
|
|
532
|
-
v-if="store.preflightResults?.length"
|
|
533
|
-
class="space-y-2"
|
|
534
|
-
data-testid="env-setup-preflight-results"
|
|
535
|
-
>
|
|
536
|
-
<li
|
|
537
|
-
v-for="r in store.preflightResults"
|
|
538
|
-
:key="r.title"
|
|
539
|
-
class="rounded border border-slate-800 bg-slate-900/40 p-2"
|
|
540
|
-
>
|
|
541
|
-
<div class="flex items-center gap-2">
|
|
542
|
-
<UBadge :color="PREFLIGHT_COLOR[r.status]" variant="subtle" size="sm">
|
|
543
|
-
{{ r.status }}
|
|
544
|
-
</UBadge>
|
|
545
|
-
<span class="text-[12px] text-slate-200">{{ r.title }}</span>
|
|
546
|
-
<span v-if="!r.required" class="ms-auto text-[10px] text-slate-500">
|
|
547
|
-
{{ t('environmentWizard.preflight.optional') }}
|
|
94
|
+
{{ STEP_LABEL[s] }}
|
|
548
95
|
</span>
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
</li>
|
|
557
|
-
</ul>
|
|
558
|
-
</section>
|
|
559
|
-
|
|
560
|
-
<!-- ============================ SAVE ============================ -->
|
|
561
|
-
<section v-else class="space-y-3" data-testid="env-setup-step-save">
|
|
562
|
-
<UFormField
|
|
563
|
-
:label="t('environmentWizard.save.handlerLabel')"
|
|
564
|
-
:description="t('environmentWizard.save.handlerHint')"
|
|
565
|
-
>
|
|
566
|
-
<UInput
|
|
567
|
-
v-model="store.handlerLabel"
|
|
568
|
-
class="w-full"
|
|
569
|
-
data-testid="env-setup-handler-label"
|
|
570
|
-
/>
|
|
571
|
-
</UFormField>
|
|
572
|
-
|
|
573
|
-
<div
|
|
574
|
-
class="rounded-md border border-slate-800 bg-slate-900/40 p-3 text-[12px] text-slate-300"
|
|
575
|
-
>
|
|
576
|
-
<p>
|
|
577
|
-
{{
|
|
578
|
-
t('environmentWizard.save.summary', {
|
|
579
|
-
frame: store.targetFrame?.title ?? '',
|
|
580
|
-
service: store.composeService,
|
|
581
|
-
})
|
|
582
|
-
}}
|
|
583
|
-
</p>
|
|
584
|
-
</div>
|
|
585
|
-
|
|
586
|
-
<p
|
|
587
|
-
v-if="store.saveError"
|
|
588
|
-
class="text-[12px] text-rose-300/80"
|
|
589
|
-
data-testid="env-setup-save-error"
|
|
590
|
-
>
|
|
591
|
-
{{ store.saveError }}
|
|
592
|
-
</p>
|
|
593
|
-
|
|
594
|
-
<div v-if="!store.saved" class="flex justify-end">
|
|
595
|
-
<UButton
|
|
596
|
-
color="primary"
|
|
597
|
-
icon="i-lucide-save"
|
|
598
|
-
:loading="store.saving"
|
|
599
|
-
:disabled="!store.composeService.trim()"
|
|
600
|
-
data-testid="env-setup-save"
|
|
601
|
-
@click="store.save()"
|
|
602
|
-
>
|
|
603
|
-
{{ t('environmentWizard.save.save') }}
|
|
604
|
-
</UButton>
|
|
605
|
-
</div>
|
|
606
|
-
|
|
607
|
-
<!-- saved: confirmation + optional trial provision -->
|
|
608
|
-
<template v-else>
|
|
609
|
-
<div
|
|
610
|
-
class="flex items-center gap-2 rounded-md border border-emerald-800/50 bg-emerald-950/30 p-2 text-[12px] text-emerald-200"
|
|
611
|
-
data-testid="env-setup-saved"
|
|
612
|
-
>
|
|
613
|
-
<UIcon name="i-lucide-check-circle" class="h-4 w-4" />
|
|
614
|
-
{{ t('environmentWizard.save.saved') }}
|
|
615
|
-
</div>
|
|
96
|
+
<UIcon
|
|
97
|
+
v-if="i < ENV_STEP_ORDER.length - 1"
|
|
98
|
+
name="i-lucide-chevron-right"
|
|
99
|
+
class="h-3 w-3 text-slate-600"
|
|
100
|
+
/>
|
|
101
|
+
</li>
|
|
102
|
+
</ol>
|
|
616
103
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
<
|
|
620
|
-
size="xs"
|
|
621
|
-
variant="soft"
|
|
622
|
-
color="neutral"
|
|
623
|
-
icon="i-lucide-play"
|
|
624
|
-
:loading="store.trialing"
|
|
625
|
-
:disabled="store.trialStarted"
|
|
626
|
-
data-testid="env-setup-trial"
|
|
627
|
-
@click="store.trialProvision()"
|
|
628
|
-
>
|
|
629
|
-
{{ t('environmentWizard.trial.run') }}
|
|
630
|
-
</UButton>
|
|
104
|
+
<!-- the current step (pick / review / preflight / save) -->
|
|
105
|
+
<div class="mt-4">
|
|
106
|
+
<JourneyOutlet :instance-id="instanceId" />
|
|
631
107
|
</div>
|
|
632
|
-
<p v-if="store.trialError" class="text-[11px] text-rose-300/80">
|
|
633
|
-
{{ store.trialError }}
|
|
634
|
-
</p>
|
|
635
|
-
<ProvisioningLogsDrawer v-if="store.trialStarted" subsystem="environment" />
|
|
636
108
|
</template>
|
|
637
|
-
</
|
|
638
|
-
|
|
639
|
-
<!-- footer nav -->
|
|
640
|
-
<div class="flex items-center justify-between border-t border-slate-800 pt-3">
|
|
641
|
-
<UButton
|
|
642
|
-
color="neutral"
|
|
643
|
-
variant="ghost"
|
|
644
|
-
:disabled="currentIndex === 0"
|
|
645
|
-
icon="i-lucide-arrow-left"
|
|
646
|
-
data-testid="env-setup-back"
|
|
647
|
-
@click="back"
|
|
648
|
-
>
|
|
649
|
-
{{ t('common.back') }}
|
|
650
|
-
</UButton>
|
|
651
|
-
<UButton
|
|
652
|
-
v-if="store.step !== 'save'"
|
|
653
|
-
color="primary"
|
|
654
|
-
trailing-icon="i-lucide-arrow-right"
|
|
655
|
-
:disabled="(store.step === 'review' && !canLeaveReview) || store.step === 'pick'"
|
|
656
|
-
data-testid="env-setup-next"
|
|
657
|
-
@click="next"
|
|
658
|
-
>
|
|
659
|
-
{{ t('common.next') }}
|
|
660
|
-
</UButton>
|
|
661
|
-
<UButton
|
|
662
|
-
v-else
|
|
663
|
-
color="neutral"
|
|
664
|
-
variant="soft"
|
|
665
|
-
icon="i-lucide-check"
|
|
666
|
-
data-testid="env-setup-done"
|
|
667
|
-
@click="ui.closeEnvironmentSetup()"
|
|
668
|
-
>
|
|
669
|
-
{{ t('common.done') }}
|
|
670
|
-
</UButton>
|
|
671
|
-
</div>
|
|
109
|
+
</JourneyHost>
|
|
672
110
|
</div>
|
|
673
111
|
</template>
|
|
674
112
|
</UModal>
|