@cat-factory/app 0.75.2 → 0.76.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/app/components/panels/inspector/FrontendConfig.vue +536 -269
- package/app/composables/api/infraHandlers.ts +6 -0
- package/app/stores/infraConfig.ts +11 -0
- package/app/types/domain.ts +2 -0
- package/i18n/locales/en.json +24 -1
- package/i18n/locales/es.json +24 -1
- package/i18n/locales/fr.json +24 -1
- package/i18n/locales/he.json +24 -1
- package/i18n/locales/ja.json +24 -1
- package/i18n/locales/pl.json +24 -1
- package/i18n/locales/tr.json +24 -1
- package/i18n/locales/uk.json +24 -1
- package/package.json +2 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, onUnmounted, watch } from 'vue'
|
|
2
|
+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
3
3
|
import type {
|
|
4
4
|
Block,
|
|
5
5
|
FrontendBackendBinding,
|
|
6
6
|
FrontendConfig,
|
|
7
|
+
FrontendConfigRecommendation,
|
|
7
8
|
FrontendEnvInjection,
|
|
8
9
|
FrontendPackageManager,
|
|
9
10
|
FrontendServeMode,
|
|
@@ -15,12 +16,16 @@ import type {
|
|
|
15
16
|
// and its backend bindings. Each binding names an env var the frontend reads for an upstream
|
|
16
17
|
// URL and where that URL resolves — a bound SERVICE frame's ephemeral env (the service under
|
|
17
18
|
// test), or WireMock. The bindings ARE the board's frontend→service links. Persisted as a
|
|
18
|
-
// serialized FrontendConfig on the block via the shared updateBlock PATCH.
|
|
19
|
+
// serialized FrontendConfig on the block via the shared updateBlock PATCH. The fields are grouped
|
|
20
|
+
// into collapsible sections (all collapsed by default), with a repo-driven "Detect from repo"
|
|
21
|
+
// affordance that proposes most of them for the user to review + apply.
|
|
19
22
|
const props = defineProps<{ block: Block }>()
|
|
20
23
|
|
|
21
24
|
const board = useBoardStore()
|
|
22
25
|
const auth = useAuthStore()
|
|
23
26
|
const preview = usePreviewStore()
|
|
27
|
+
const infra = useInfraConfigStore()
|
|
28
|
+
const github = useGitHubStore()
|
|
24
29
|
const { t } = useI18n()
|
|
25
30
|
|
|
26
31
|
// A browsable preview needs a long-lived host serve, so it is a local/node runtime capability
|
|
@@ -97,6 +102,77 @@ function removeBinding(index: number) {
|
|
|
97
102
|
save({ backendBindings: bindings.value.filter((_, i) => i !== index) })
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
// ---- Collapsible groups (all collapsed by default) --------------------------------------
|
|
106
|
+
const showBuild = ref(false)
|
|
107
|
+
const showServe = ref(false)
|
|
108
|
+
const showMocking = ref(false)
|
|
109
|
+
const showEnv = ref(false)
|
|
110
|
+
const showBindings = ref(false)
|
|
111
|
+
const showPreview = ref(false)
|
|
112
|
+
|
|
113
|
+
// ---- Detect from repo (preview → apply) --------------------------------------------------
|
|
114
|
+
// Read the frontend repo checkout-free and propose a NON-BINDING recommended config. The result
|
|
115
|
+
// is held as a preview the user reviews; NOTHING is persisted until they click Apply. The frontend
|
|
116
|
+
// frame links to its repo the same way a service does (`github_repos.block_id`).
|
|
117
|
+
const repoLink = computed(() => github.repoForBlock(props.block.id))
|
|
118
|
+
const detecting = ref(false)
|
|
119
|
+
const detectError = ref(false)
|
|
120
|
+
const detectResult = ref<FrontendConfigRecommendation | null>(null)
|
|
121
|
+
|
|
122
|
+
// A detection result is scoped to the inspected block — clear it (and any error) when the
|
|
123
|
+
// selection changes, so block B never shows block A's stale recommendation.
|
|
124
|
+
watch(
|
|
125
|
+
() => props.block.id,
|
|
126
|
+
() => {
|
|
127
|
+
detectResult.value = null
|
|
128
|
+
detectError.value = false
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async function detectFromRepo() {
|
|
133
|
+
const repo = repoLink.value
|
|
134
|
+
if (!repo) {
|
|
135
|
+
detectError.value = true
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
detecting.value = true
|
|
139
|
+
detectError.value = false
|
|
140
|
+
detectResult.value = null
|
|
141
|
+
try {
|
|
142
|
+
const result = await infra.detectFrontendConfig({
|
|
143
|
+
owner: repo.owner,
|
|
144
|
+
repo: repo.name,
|
|
145
|
+
...(config.value.directory ? { directory: config.value.directory } : {}),
|
|
146
|
+
})
|
|
147
|
+
detectResult.value = result
|
|
148
|
+
// When nothing was detected the "none" hint tells the user to set the frontend directory —
|
|
149
|
+
// that field lives in the (collapsed) Build group, so open it so the advice is actionable.
|
|
150
|
+
if (!result.detected) showBuild.value = true
|
|
151
|
+
} catch {
|
|
152
|
+
detectError.value = true
|
|
153
|
+
} finally {
|
|
154
|
+
detecting.value = false
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Apply the recommendation: overlay the proposed scalar fields, but APPEND backend bindings
|
|
159
|
+
// (never overwrite existing rows or their `source` — that would wipe configured service links).
|
|
160
|
+
// Only env-var names not already present are added.
|
|
161
|
+
function applyDetection() {
|
|
162
|
+
const rec = detectResult.value
|
|
163
|
+
if (!rec) return
|
|
164
|
+
const { backendBindings: proposed, ...rest } = rec.config
|
|
165
|
+
const existing = bindings.value
|
|
166
|
+
const existingNames = new Set(existing.map((b) => b.envVar.trim()).filter(Boolean))
|
|
167
|
+
const added = (proposed ?? []).filter((b) => b.envVar && !existingNames.has(b.envVar.trim()))
|
|
168
|
+
save({ ...rest, backendBindings: [...existing, ...added] })
|
|
169
|
+
// Open the groups so the user sees what changed.
|
|
170
|
+
showBuild.value = true
|
|
171
|
+
showServe.value = true
|
|
172
|
+
if (added.length) showBindings.value = true
|
|
173
|
+
detectResult.value = null
|
|
174
|
+
}
|
|
175
|
+
|
|
100
176
|
// ---- Browsable preview (live runtime state, distinct from the persisted toggle) ----------
|
|
101
177
|
// The live preview is a separate resource fetched from the preview endpoints; the store keys it
|
|
102
178
|
// by frame id and self-polls while it is `starting`. Only relevant on a preview-capable runtime
|
|
@@ -140,139 +216,290 @@ onUnmounted(() => preview.stopPolling(props.block.id))
|
|
|
140
216
|
{{ t('inspector.frontendConfig.hint') }}
|
|
141
217
|
</p>
|
|
142
218
|
|
|
143
|
-
<!--
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
219
|
+
<!-- Detect from repo: propose a config the user reviews + applies. Non-binding — nothing is
|
|
220
|
+
persisted until Apply. Only shown when the frame is linked to a repo. -->
|
|
221
|
+
<div
|
|
222
|
+
v-if="repoLink"
|
|
223
|
+
class="space-y-2 rounded border border-slate-800 bg-slate-900/40 p-2"
|
|
224
|
+
data-testid="frontend-detect"
|
|
225
|
+
>
|
|
226
|
+
<div class="flex items-center justify-between gap-2">
|
|
227
|
+
<span class="text-[11px] text-slate-400">{{
|
|
228
|
+
t('inspector.frontendConfig.detect.title')
|
|
229
|
+
}}</span>
|
|
149
230
|
<UButton
|
|
150
|
-
v-for="pm in PACKAGE_MANAGERS"
|
|
151
|
-
:key="pm"
|
|
152
|
-
:color="packageManager === pm ? 'primary' : 'neutral'"
|
|
153
|
-
:variant="packageManager === pm ? 'soft' : 'ghost'"
|
|
154
231
|
size="xs"
|
|
155
|
-
|
|
232
|
+
variant="soft"
|
|
233
|
+
color="primary"
|
|
234
|
+
icon="i-lucide-wand-sparkles"
|
|
235
|
+
:loading="detecting"
|
|
236
|
+
data-testid="frontend-detect-button"
|
|
237
|
+
@click="detectFromRepo"
|
|
156
238
|
>
|
|
157
|
-
{{
|
|
239
|
+
{{ t('inspector.frontendConfig.detect.button') }}
|
|
158
240
|
</UButton>
|
|
159
241
|
</div>
|
|
160
|
-
|
|
242
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
243
|
+
{{ t('inspector.frontendConfig.detect.hint') }}
|
|
244
|
+
</p>
|
|
245
|
+
|
|
246
|
+
<p v-if="detectError" class="text-[11px] text-rose-300/80">
|
|
247
|
+
{{ t('inspector.frontendConfig.detect.error') }}
|
|
248
|
+
</p>
|
|
249
|
+
|
|
250
|
+
<template v-if="detectResult && !detecting">
|
|
251
|
+
<p v-if="!detectResult.detected" class="text-[11px] text-amber-300/80">
|
|
252
|
+
{{ t('inspector.frontendConfig.detect.none') }}
|
|
253
|
+
</p>
|
|
161
254
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
255
|
+
<ul v-if="detectResult.notes.length" class="space-y-0.5">
|
|
256
|
+
<li
|
|
257
|
+
v-for="(n, i) in detectResult.notes"
|
|
258
|
+
:key="i"
|
|
259
|
+
class="flex items-start gap-1.5 text-[11px] leading-snug text-slate-500"
|
|
260
|
+
>
|
|
261
|
+
<span :class="n.confidence === 'high' ? 'text-emerald-400/70' : 'text-amber-400/70'">
|
|
262
|
+
{{
|
|
263
|
+
n.confidence === 'high'
|
|
264
|
+
? t('inspector.frontendConfig.detect.confidenceHigh')
|
|
265
|
+
: t('inspector.frontendConfig.detect.confidenceLow')
|
|
266
|
+
}}
|
|
267
|
+
</span>
|
|
268
|
+
<span>{{ n.message }}</span>
|
|
269
|
+
</li>
|
|
270
|
+
</ul>
|
|
271
|
+
|
|
272
|
+
<div v-if="detectResult.detected" class="flex flex-wrap gap-1">
|
|
273
|
+
<UButton
|
|
274
|
+
size="xs"
|
|
275
|
+
variant="soft"
|
|
276
|
+
color="primary"
|
|
277
|
+
icon="i-lucide-check"
|
|
278
|
+
data-testid="frontend-detect-apply"
|
|
279
|
+
@click="applyDetection"
|
|
280
|
+
>
|
|
281
|
+
{{ t('inspector.frontendConfig.detect.apply') }}
|
|
282
|
+
</UButton>
|
|
283
|
+
<UButton size="xs" variant="ghost" color="neutral" @click="detectResult = null">
|
|
284
|
+
{{ t('inspector.frontendConfig.detect.dismiss') }}
|
|
285
|
+
</UButton>
|
|
286
|
+
</div>
|
|
287
|
+
</template>
|
|
178
288
|
</div>
|
|
179
289
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
placeholder="build"
|
|
191
|
-
@blur="(e: FocusEvent) => saveText('buildScript', (e.target as HTMLInputElement).value)"
|
|
192
|
-
@keydown.enter="
|
|
193
|
-
(e: KeyboardEvent) => saveText('buildScript', (e.target as HTMLInputElement).value)
|
|
194
|
-
"
|
|
195
|
-
/>
|
|
196
|
-
</div>
|
|
197
|
-
<div class="space-y-1">
|
|
198
|
-
<label class="text-[11px] text-slate-400">{{
|
|
199
|
-
t('inspector.frontendConfig.outputDir')
|
|
200
|
-
}}</label>
|
|
201
|
-
<UInput
|
|
202
|
-
:model-value="config.outputDir ?? ''"
|
|
203
|
-
size="xs"
|
|
204
|
-
class="font-mono"
|
|
205
|
-
maxlength="400"
|
|
206
|
-
placeholder="dist"
|
|
207
|
-
@blur="(e: FocusEvent) => saveText('outputDir', (e.target as HTMLInputElement).value)"
|
|
208
|
-
@keydown.enter="
|
|
209
|
-
(e: KeyboardEvent) => saveText('outputDir', (e.target as HTMLInputElement).value)
|
|
210
|
-
"
|
|
290
|
+
<!-- Build: package manager + frontend directory + install command + build script + output dir -->
|
|
291
|
+
<div class="border-t border-slate-800 pt-2">
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
class="flex w-full items-center gap-1.5 text-start text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:text-slate-300"
|
|
295
|
+
@click="showBuild = !showBuild"
|
|
296
|
+
>
|
|
297
|
+
<UIcon
|
|
298
|
+
:name="showBuild ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
299
|
+
class="h-3.5 w-3.5"
|
|
211
300
|
/>
|
|
301
|
+
{{ t('inspector.frontendConfig.groups.build') }}
|
|
302
|
+
</button>
|
|
303
|
+
|
|
304
|
+
<div v-if="showBuild" class="mt-2 space-y-3">
|
|
305
|
+
<div class="space-y-1">
|
|
306
|
+
<span class="text-[11px] text-slate-400">{{
|
|
307
|
+
t('inspector.frontendConfig.packageManager')
|
|
308
|
+
}}</span>
|
|
309
|
+
<div class="flex flex-wrap gap-1">
|
|
310
|
+
<UButton
|
|
311
|
+
v-for="pm in PACKAGE_MANAGERS"
|
|
312
|
+
:key="pm"
|
|
313
|
+
:color="packageManager === pm ? 'primary' : 'neutral'"
|
|
314
|
+
:variant="packageManager === pm ? 'soft' : 'ghost'"
|
|
315
|
+
size="xs"
|
|
316
|
+
@click="save({ packageManager: pm })"
|
|
317
|
+
>
|
|
318
|
+
{{ pm }}
|
|
319
|
+
</UButton>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div class="space-y-1">
|
|
324
|
+
<label class="text-[11px] text-slate-400">{{
|
|
325
|
+
t('inspector.frontendConfig.directory')
|
|
326
|
+
}}</label>
|
|
327
|
+
<UInput
|
|
328
|
+
:model-value="config.directory ?? ''"
|
|
329
|
+
size="xs"
|
|
330
|
+
class="font-mono"
|
|
331
|
+
maxlength="400"
|
|
332
|
+
placeholder="frontend/"
|
|
333
|
+
@blur="(e: FocusEvent) => saveText('directory', (e.target as HTMLInputElement).value)"
|
|
334
|
+
@keydown.enter="
|
|
335
|
+
(e: KeyboardEvent) => saveText('directory', (e.target as HTMLInputElement).value)
|
|
336
|
+
"
|
|
337
|
+
/>
|
|
338
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
339
|
+
{{ t('inspector.frontendConfig.directoryHint') }}
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div class="space-y-1">
|
|
344
|
+
<label class="text-[11px] text-slate-400">{{
|
|
345
|
+
t('inspector.frontendConfig.installCommand')
|
|
346
|
+
}}</label>
|
|
347
|
+
<UInput
|
|
348
|
+
:model-value="config.installCommand ?? ''"
|
|
349
|
+
size="xs"
|
|
350
|
+
class="font-mono"
|
|
351
|
+
maxlength="400"
|
|
352
|
+
placeholder="pnpm install --frozen-lockfile"
|
|
353
|
+
@blur="
|
|
354
|
+
(e: FocusEvent) => saveText('installCommand', (e.target as HTMLInputElement).value)
|
|
355
|
+
"
|
|
356
|
+
@keydown.enter="
|
|
357
|
+
(e: KeyboardEvent) => saveText('installCommand', (e.target as HTMLInputElement).value)
|
|
358
|
+
"
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
<div class="grid grid-cols-2 gap-2">
|
|
363
|
+
<div class="space-y-1">
|
|
364
|
+
<label class="text-[11px] text-slate-400">{{
|
|
365
|
+
t('inspector.frontendConfig.buildScript')
|
|
366
|
+
}}</label>
|
|
367
|
+
<UInput
|
|
368
|
+
:model-value="config.buildScript ?? ''"
|
|
369
|
+
size="xs"
|
|
370
|
+
class="font-mono"
|
|
371
|
+
maxlength="200"
|
|
372
|
+
placeholder="build"
|
|
373
|
+
@blur="
|
|
374
|
+
(e: FocusEvent) => saveText('buildScript', (e.target as HTMLInputElement).value)
|
|
375
|
+
"
|
|
376
|
+
@keydown.enter="
|
|
377
|
+
(e: KeyboardEvent) => saveText('buildScript', (e.target as HTMLInputElement).value)
|
|
378
|
+
"
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="space-y-1">
|
|
382
|
+
<label class="text-[11px] text-slate-400">{{
|
|
383
|
+
t('inspector.frontendConfig.outputDir')
|
|
384
|
+
}}</label>
|
|
385
|
+
<UInput
|
|
386
|
+
:model-value="config.outputDir ?? ''"
|
|
387
|
+
size="xs"
|
|
388
|
+
class="font-mono"
|
|
389
|
+
maxlength="400"
|
|
390
|
+
placeholder="dist"
|
|
391
|
+
@blur="(e: FocusEvent) => saveText('outputDir', (e.target as HTMLInputElement).value)"
|
|
392
|
+
@keydown.enter="
|
|
393
|
+
(e: KeyboardEvent) => saveText('outputDir', (e.target as HTMLInputElement).value)
|
|
394
|
+
"
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
212
398
|
</div>
|
|
213
399
|
</div>
|
|
214
400
|
|
|
215
401
|
<!-- Serve: mode (static vs command) + serve script (command mode) + port -->
|
|
216
|
-
<div class="
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
402
|
+
<div class="border-t border-slate-800 pt-2">
|
|
403
|
+
<button
|
|
404
|
+
type="button"
|
|
405
|
+
class="flex w-full items-center gap-1.5 text-start text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:text-slate-300"
|
|
406
|
+
@click="showServe = !showServe"
|
|
407
|
+
>
|
|
408
|
+
<UIcon
|
|
409
|
+
:name="showServe ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
410
|
+
class="h-3.5 w-3.5"
|
|
411
|
+
/>
|
|
412
|
+
{{ t('inspector.frontendConfig.groups.serve') }}
|
|
413
|
+
</button>
|
|
414
|
+
|
|
415
|
+
<div v-if="showServe" class="mt-2 space-y-3">
|
|
416
|
+
<div class="space-y-1">
|
|
417
|
+
<span class="text-[11px] text-slate-400">{{
|
|
418
|
+
t('inspector.frontendConfig.serveMode')
|
|
419
|
+
}}</span>
|
|
420
|
+
<div class="flex flex-wrap gap-1">
|
|
421
|
+
<UButton
|
|
422
|
+
:color="serveMode === 'static' ? 'primary' : 'neutral'"
|
|
423
|
+
:variant="serveMode === 'static' ? 'soft' : 'ghost'"
|
|
424
|
+
size="xs"
|
|
425
|
+
@click="save({ serveMode: 'static' })"
|
|
426
|
+
>
|
|
427
|
+
{{ t('inspector.frontendConfig.serveStatic') }}
|
|
428
|
+
</UButton>
|
|
429
|
+
<UButton
|
|
430
|
+
:color="serveMode === 'command' ? 'primary' : 'neutral'"
|
|
431
|
+
:variant="serveMode === 'command' ? 'soft' : 'ghost'"
|
|
432
|
+
size="xs"
|
|
433
|
+
@click="save({ serveMode: 'command' })"
|
|
434
|
+
>
|
|
435
|
+
{{ t('inspector.frontendConfig.serveCommand') }}
|
|
436
|
+
</UButton>
|
|
437
|
+
</div>
|
|
438
|
+
<!-- Explain each mode, and disambiguate from the separate envInjection axis. -->
|
|
439
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
440
|
+
<span class="text-slate-400">{{ t('inspector.frontendConfig.serveStatic') }}:</span>
|
|
441
|
+
{{ t('inspector.frontendConfig.serveStaticDesc') }}
|
|
442
|
+
</p>
|
|
443
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
444
|
+
<span class="text-slate-400">{{ t('inspector.frontendConfig.serveCommand') }}:</span>
|
|
445
|
+
{{ t('inspector.frontendConfig.serveCommandDesc') }}
|
|
446
|
+
</p>
|
|
447
|
+
<p class="text-[11px] leading-snug text-slate-500/80">
|
|
448
|
+
{{ t('inspector.frontendConfig.serveEnvAxisNote') }}
|
|
449
|
+
</p>
|
|
450
|
+
</div>
|
|
240
451
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
452
|
+
<div v-if="serveMode === 'command'" class="space-y-1">
|
|
453
|
+
<label class="text-[11px] text-slate-400">{{
|
|
454
|
+
t('inspector.frontendConfig.serveScript')
|
|
455
|
+
}}</label>
|
|
456
|
+
<UInput
|
|
457
|
+
:model-value="config.serveScript ?? ''"
|
|
458
|
+
size="xs"
|
|
459
|
+
class="font-mono"
|
|
460
|
+
maxlength="200"
|
|
461
|
+
placeholder="preview"
|
|
462
|
+
@blur="(e: FocusEvent) => saveText('serveScript', (e.target as HTMLInputElement).value)"
|
|
463
|
+
@keydown.enter="
|
|
464
|
+
(e: KeyboardEvent) => saveText('serveScript', (e.target as HTMLInputElement).value)
|
|
465
|
+
"
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<div class="space-y-1">
|
|
470
|
+
<label class="text-[11px] text-slate-400">{{
|
|
471
|
+
t('inspector.frontendConfig.servePort')
|
|
472
|
+
}}</label>
|
|
473
|
+
<UInput
|
|
474
|
+
:model-value="config.servePort != null ? String(config.servePort) : ''"
|
|
475
|
+
type="number"
|
|
476
|
+
min="1"
|
|
477
|
+
max="65535"
|
|
478
|
+
step="1"
|
|
479
|
+
size="xs"
|
|
480
|
+
class="font-mono"
|
|
481
|
+
placeholder="4173"
|
|
482
|
+
@blur="(e: FocusEvent) => saveServePort((e.target as HTMLInputElement).value)"
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
256
486
|
</div>
|
|
257
487
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
step="1"
|
|
269
|
-
size="xs"
|
|
270
|
-
class="font-mono"
|
|
271
|
-
placeholder="4173"
|
|
272
|
-
@blur="(e: FocusEvent) => saveServePort((e.target as HTMLInputElement).value)"
|
|
488
|
+
<!-- Mocking: WireMock mappings dir -->
|
|
489
|
+
<div class="border-t border-slate-800 pt-2">
|
|
490
|
+
<button
|
|
491
|
+
type="button"
|
|
492
|
+
class="flex w-full items-center gap-1.5 text-start text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:text-slate-300"
|
|
493
|
+
@click="showMocking = !showMocking"
|
|
494
|
+
>
|
|
495
|
+
<UIcon
|
|
496
|
+
:name="showMocking ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
497
|
+
class="h-3.5 w-3.5"
|
|
273
498
|
/>
|
|
274
|
-
|
|
275
|
-
|
|
499
|
+
{{ t('inspector.frontendConfig.groups.mocking') }}
|
|
500
|
+
</button>
|
|
501
|
+
|
|
502
|
+
<div v-if="showMocking" class="mt-2 space-y-1">
|
|
276
503
|
<label class="text-[11px] text-slate-400">{{
|
|
277
504
|
t('inspector.frontendConfig.mockMappingsPath')
|
|
278
505
|
}}</label>
|
|
@@ -289,185 +516,225 @@ onUnmounted(() => preview.stopPolling(props.block.id))
|
|
|
289
516
|
(e: KeyboardEvent) => saveText('mockMappingsPath', (e.target as HTMLInputElement).value)
|
|
290
517
|
"
|
|
291
518
|
/>
|
|
292
|
-
<p class="
|
|
519
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
293
520
|
{{ t('inspector.frontendConfig.mockMappingsHint') }}
|
|
294
521
|
</p>
|
|
295
522
|
</div>
|
|
296
523
|
</div>
|
|
297
524
|
|
|
298
525
|
<!-- Env injection: build-time env vars vs a runtime window.env shim -->
|
|
299
|
-
<div class="
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
526
|
+
<div class="border-t border-slate-800 pt-2">
|
|
527
|
+
<button
|
|
528
|
+
type="button"
|
|
529
|
+
class="flex w-full items-center gap-1.5 text-start text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:text-slate-300"
|
|
530
|
+
@click="showEnv = !showEnv"
|
|
531
|
+
>
|
|
532
|
+
<UIcon
|
|
533
|
+
:name="showEnv ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
534
|
+
class="h-3.5 w-3.5"
|
|
535
|
+
/>
|
|
536
|
+
{{ t('inspector.frontendConfig.groups.envInjection') }}
|
|
537
|
+
</button>
|
|
538
|
+
|
|
539
|
+
<div v-if="showEnv" class="mt-2 space-y-1">
|
|
540
|
+
<div class="flex flex-wrap gap-1">
|
|
541
|
+
<UButton
|
|
542
|
+
:color="envInjection === 'build' ? 'primary' : 'neutral'"
|
|
543
|
+
:variant="envInjection === 'build' ? 'soft' : 'ghost'"
|
|
544
|
+
size="xs"
|
|
545
|
+
@click="save({ envInjection: 'build' })"
|
|
546
|
+
>
|
|
547
|
+
{{ t('inspector.frontendConfig.envBuild') }}
|
|
548
|
+
</UButton>
|
|
549
|
+
<UButton
|
|
550
|
+
:color="envInjection === 'runtime' ? 'primary' : 'neutral'"
|
|
551
|
+
:variant="envInjection === 'runtime' ? 'soft' : 'ghost'"
|
|
552
|
+
size="xs"
|
|
553
|
+
@click="save({ envInjection: 'runtime' })"
|
|
554
|
+
>
|
|
555
|
+
{{ t('inspector.frontendConfig.envRuntime') }}
|
|
556
|
+
</UButton>
|
|
557
|
+
</div>
|
|
558
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
559
|
+
{{ t('inspector.frontendConfig.envInjectionHint') }}
|
|
560
|
+
</p>
|
|
320
561
|
</div>
|
|
321
|
-
<p class="text-[11px] leading-snug text-slate-500">
|
|
322
|
-
{{ t('inspector.frontendConfig.envInjectionHint') }}
|
|
323
|
-
</p>
|
|
324
562
|
</div>
|
|
325
563
|
|
|
326
564
|
<!-- Backend bindings: env var → upstream. These double as the board's frontend→service links. -->
|
|
327
|
-
<div class="
|
|
328
|
-
<
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
icon="i-lucide-plus"
|
|
337
|
-
@click="addBinding"
|
|
565
|
+
<div class="border-t border-slate-800 pt-2">
|
|
566
|
+
<button
|
|
567
|
+
type="button"
|
|
568
|
+
class="flex w-full items-center gap-1.5 text-start text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:text-slate-300"
|
|
569
|
+
@click="showBindings = !showBindings"
|
|
570
|
+
>
|
|
571
|
+
<UIcon
|
|
572
|
+
:name="showBindings ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
573
|
+
class="h-3.5 w-3.5"
|
|
338
574
|
/>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
575
|
+
{{ t('inspector.frontendConfig.groups.bindings') }}
|
|
576
|
+
<span v-if="bindings.length" class="font-normal normal-case text-slate-600"
|
|
577
|
+
>({{ bindings.length }})</span
|
|
578
|
+
>
|
|
579
|
+
</button>
|
|
343
580
|
|
|
344
|
-
<div v-if="
|
|
345
|
-
<div
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
class="flex-1 font-mono"
|
|
350
|
-
maxlength="200"
|
|
351
|
-
placeholder="PUB_BACKEND_URL"
|
|
352
|
-
@blur="(e: FocusEvent) => setBindingEnvVar(i, (e.target as HTMLInputElement).value)"
|
|
353
|
-
@keydown.enter="
|
|
354
|
-
(e: KeyboardEvent) => setBindingEnvVar(i, (e.target as HTMLInputElement).value)
|
|
355
|
-
"
|
|
356
|
-
/>
|
|
357
|
-
<USelect
|
|
358
|
-
:model-value="sourceValue(b)"
|
|
359
|
-
:items="sourceItems"
|
|
360
|
-
size="xs"
|
|
361
|
-
class="flex-1"
|
|
362
|
-
@update:model-value="(v: string) => setBindingSource(i, v)"
|
|
363
|
-
/>
|
|
581
|
+
<div v-if="showBindings" class="mt-2 space-y-2">
|
|
582
|
+
<div class="flex items-center justify-between">
|
|
583
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
584
|
+
{{ t('inspector.frontendConfig.bindings.hint') }}
|
|
585
|
+
</p>
|
|
364
586
|
<UButton
|
|
365
587
|
size="xs"
|
|
366
588
|
variant="ghost"
|
|
367
589
|
color="neutral"
|
|
368
|
-
icon="i-lucide-
|
|
369
|
-
|
|
370
|
-
@click="removeBinding(i)"
|
|
590
|
+
icon="i-lucide-plus"
|
|
591
|
+
@click="addBinding"
|
|
371
592
|
/>
|
|
372
593
|
</div>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
594
|
+
|
|
595
|
+
<div v-if="bindings.length" class="space-y-1.5">
|
|
596
|
+
<div v-for="(b, i) in bindings" :key="i" class="flex items-center gap-1">
|
|
597
|
+
<UInput
|
|
598
|
+
:model-value="b.envVar"
|
|
599
|
+
size="xs"
|
|
600
|
+
class="flex-1 font-mono"
|
|
601
|
+
maxlength="200"
|
|
602
|
+
placeholder="PUB_BACKEND_URL"
|
|
603
|
+
@blur="(e: FocusEvent) => setBindingEnvVar(i, (e.target as HTMLInputElement).value)"
|
|
604
|
+
@keydown.enter="
|
|
605
|
+
(e: KeyboardEvent) => setBindingEnvVar(i, (e.target as HTMLInputElement).value)
|
|
606
|
+
"
|
|
607
|
+
/>
|
|
608
|
+
<USelect
|
|
609
|
+
:model-value="sourceValue(b)"
|
|
610
|
+
:items="sourceItems"
|
|
611
|
+
size="xs"
|
|
612
|
+
class="flex-1"
|
|
613
|
+
@update:model-value="(v: string) => setBindingSource(i, v)"
|
|
614
|
+
/>
|
|
615
|
+
<UButton
|
|
616
|
+
size="xs"
|
|
617
|
+
variant="ghost"
|
|
618
|
+
color="neutral"
|
|
619
|
+
icon="i-lucide-x"
|
|
620
|
+
:title="t('inspector.frontendConfig.bindings.remove')"
|
|
621
|
+
@click="removeBinding(i)"
|
|
622
|
+
/>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
<div v-else class="text-[11px] text-slate-500">
|
|
626
|
+
{{ t('inspector.frontendConfig.bindings.empty') }}
|
|
627
|
+
</div>
|
|
376
628
|
</div>
|
|
377
629
|
</div>
|
|
378
630
|
|
|
379
631
|
<!-- Browsable preview (local/node only; the Worker reports it unsupported). -->
|
|
380
632
|
<div class="border-t border-slate-800 pt-2">
|
|
381
|
-
<
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
633
|
+
<button
|
|
634
|
+
type="button"
|
|
635
|
+
class="flex w-full items-center gap-1.5 text-start text-[11px] font-semibold uppercase tracking-wide text-slate-500 hover:text-slate-300"
|
|
636
|
+
@click="showPreview = !showPreview"
|
|
637
|
+
>
|
|
638
|
+
<UIcon
|
|
639
|
+
:name="showPreview ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
|
640
|
+
class="h-3.5 w-3.5"
|
|
641
|
+
/>
|
|
642
|
+
{{ t('inspector.frontendConfig.groups.preview') }}
|
|
643
|
+
</button>
|
|
644
|
+
|
|
645
|
+
<div v-if="showPreview" class="mt-2">
|
|
646
|
+
<UCheckbox
|
|
647
|
+
:model-value="previewSupported && config.previewEnabled === true"
|
|
648
|
+
:label="t('inspector.frontendConfig.previewEnabled')"
|
|
649
|
+
:disabled="!previewSupported"
|
|
650
|
+
size="xs"
|
|
651
|
+
@update:model-value="
|
|
652
|
+
(v: boolean | 'indeterminate') =>
|
|
653
|
+
save({ previewEnabled: v === true ? true : undefined })
|
|
654
|
+
"
|
|
655
|
+
/>
|
|
656
|
+
<p class="mt-1 text-[11px] leading-snug text-slate-500">
|
|
657
|
+
{{
|
|
658
|
+
previewSupported
|
|
659
|
+
? t('inspector.frontendConfig.previewHint')
|
|
660
|
+
: t('inspector.frontendConfig.previewUnsupported')
|
|
661
|
+
}}
|
|
662
|
+
</p>
|
|
397
663
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
664
|
+
<!-- Live preview control: start / open (clickable URL) / stop, reflecting the runtime state. -->
|
|
665
|
+
<div v-if="previewActive" class="mt-2 space-y-1.5" data-testid="preview-panel">
|
|
666
|
+
<div class="flex items-center gap-2">
|
|
667
|
+
<span
|
|
668
|
+
class="text-[11px] font-medium"
|
|
669
|
+
:class="{
|
|
670
|
+
'text-emerald-400': previewStatus === 'ready',
|
|
671
|
+
'text-amber-400': previewStatus === 'starting',
|
|
672
|
+
'text-rose-400': previewStatus === 'failed',
|
|
673
|
+
'text-slate-400': previewStatus === 'stopped',
|
|
674
|
+
}"
|
|
675
|
+
data-testid="preview-status"
|
|
676
|
+
>
|
|
677
|
+
{{ previewStatusLabel }}
|
|
678
|
+
</span>
|
|
679
|
+
|
|
680
|
+
<UButton
|
|
681
|
+
v-if="previewStatus === 'ready' && previewState?.url"
|
|
682
|
+
:to="previewState.url"
|
|
683
|
+
target="_blank"
|
|
684
|
+
rel="noopener"
|
|
685
|
+
size="xs"
|
|
686
|
+
variant="soft"
|
|
687
|
+
color="primary"
|
|
688
|
+
trailing-icon="i-lucide-external-link"
|
|
689
|
+
data-testid="preview-url"
|
|
690
|
+
>
|
|
691
|
+
{{ t('inspector.frontendConfig.preview.open') }}
|
|
692
|
+
</UButton>
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
<div class="flex flex-wrap gap-1">
|
|
696
|
+
<UButton
|
|
697
|
+
v-if="previewStatus === 'stopped' || previewStatus === 'failed'"
|
|
698
|
+
size="xs"
|
|
699
|
+
variant="soft"
|
|
700
|
+
color="primary"
|
|
701
|
+
icon="i-lucide-play"
|
|
702
|
+
:loading="previewBusy"
|
|
703
|
+
data-testid="preview-start"
|
|
704
|
+
@click="preview.start(props.block.id)"
|
|
705
|
+
>
|
|
706
|
+
{{ t('inspector.frontendConfig.preview.start') }}
|
|
707
|
+
</UButton>
|
|
708
|
+
<UButton
|
|
709
|
+
v-if="previewStatus === 'ready' || previewStatus === 'starting'"
|
|
710
|
+
size="xs"
|
|
711
|
+
variant="ghost"
|
|
712
|
+
color="neutral"
|
|
713
|
+
icon="i-lucide-square"
|
|
714
|
+
:loading="previewBusy"
|
|
715
|
+
data-testid="preview-stop"
|
|
716
|
+
@click="preview.stop(props.block.id)"
|
|
717
|
+
>
|
|
718
|
+
{{ t('inspector.frontendConfig.preview.stop') }}
|
|
719
|
+
</UButton>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<p
|
|
723
|
+
v-if="previewStatus === 'failed' && previewState?.error"
|
|
724
|
+
class="text-[11px] leading-snug text-rose-400"
|
|
725
|
+
data-testid="preview-error"
|
|
410
726
|
>
|
|
411
|
-
{{
|
|
412
|
-
</
|
|
727
|
+
{{ previewState.error }}
|
|
728
|
+
</p>
|
|
413
729
|
|
|
414
|
-
<
|
|
415
|
-
v-if="
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
rel="noopener"
|
|
419
|
-
size="xs"
|
|
420
|
-
variant="soft"
|
|
421
|
-
color="primary"
|
|
422
|
-
trailing-icon="i-lucide-external-link"
|
|
423
|
-
data-testid="preview-url"
|
|
730
|
+
<p
|
|
731
|
+
v-if="previewRequestError"
|
|
732
|
+
class="text-[11px] leading-snug text-rose-400"
|
|
733
|
+
data-testid="preview-request-error"
|
|
424
734
|
>
|
|
425
|
-
{{
|
|
426
|
-
</
|
|
735
|
+
{{ previewRequestError }}
|
|
736
|
+
</p>
|
|
427
737
|
</div>
|
|
428
|
-
|
|
429
|
-
<div class="flex flex-wrap gap-1">
|
|
430
|
-
<UButton
|
|
431
|
-
v-if="previewStatus === 'stopped' || previewStatus === 'failed'"
|
|
432
|
-
size="xs"
|
|
433
|
-
variant="soft"
|
|
434
|
-
color="primary"
|
|
435
|
-
icon="i-lucide-play"
|
|
436
|
-
:loading="previewBusy"
|
|
437
|
-
data-testid="preview-start"
|
|
438
|
-
@click="preview.start(props.block.id)"
|
|
439
|
-
>
|
|
440
|
-
{{ t('inspector.frontendConfig.preview.start') }}
|
|
441
|
-
</UButton>
|
|
442
|
-
<UButton
|
|
443
|
-
v-if="previewStatus === 'ready' || previewStatus === 'starting'"
|
|
444
|
-
size="xs"
|
|
445
|
-
variant="ghost"
|
|
446
|
-
color="neutral"
|
|
447
|
-
icon="i-lucide-square"
|
|
448
|
-
:loading="previewBusy"
|
|
449
|
-
data-testid="preview-stop"
|
|
450
|
-
@click="preview.stop(props.block.id)"
|
|
451
|
-
>
|
|
452
|
-
{{ t('inspector.frontendConfig.preview.stop') }}
|
|
453
|
-
</UButton>
|
|
454
|
-
</div>
|
|
455
|
-
|
|
456
|
-
<p
|
|
457
|
-
v-if="previewStatus === 'failed' && previewState?.error"
|
|
458
|
-
class="text-[11px] leading-snug text-rose-400"
|
|
459
|
-
data-testid="preview-error"
|
|
460
|
-
>
|
|
461
|
-
{{ previewState.error }}
|
|
462
|
-
</p>
|
|
463
|
-
|
|
464
|
-
<p
|
|
465
|
-
v-if="previewRequestError"
|
|
466
|
-
class="text-[11px] leading-snug text-rose-400"
|
|
467
|
-
data-testid="preview-request-error"
|
|
468
|
-
>
|
|
469
|
-
{{ previewRequestError }}
|
|
470
|
-
</p>
|
|
471
738
|
</div>
|
|
472
739
|
</div>
|
|
473
740
|
</div>
|