diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index afde4ec..7a5b417 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -1929,7 +1929,13 @@ "chainSelfLabel": "This", "chainChildrenLabel": "Children", "chainPromoteButton": "Promote from parent", + "chainPromoteToChild": "Promote to this", "chainPromoting": "Promoting…", + "chainPromoteConfirmTitle": "Promote version?", + "chainPromoteConfirmMessage": "Promote the running version of {source} to {target} and deploy it?", + "chainPromoteConfirmYes": "Promote & deploy", + "chainPromoteOk": "Promoted {tag} to {target}", + "chainPromoteFailed": "Promotion failed", "chainHint": "Set parent_workload_id on a workload to build a chain. Image-source children can promote the parent's currently-running tag with one click.", "previews": { "title": "Preview environments", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index ed7906f..24da233 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -1929,7 +1929,13 @@ "chainSelfLabel": "Эта", "chainChildrenLabel": "Дочерние", "chainPromoteButton": "Продвинуть от родителя", + "chainPromoteToChild": "Продвинуть сюда", "chainPromoting": "Продвижение…", + "chainPromoteConfirmTitle": "Продвинуть версию?", + "chainPromoteConfirmMessage": "Продвинуть запущенную версию {source} в {target} и развернуть её?", + "chainPromoteConfirmYes": "Продвинуть и развернуть", + "chainPromoteOk": "Версия {tag} продвинута в {target}", + "chainPromoteFailed": "Не удалось продвинуть", "chainHint": "Задайте parent_workload_id у нагрузки, чтобы построить цепочку. Дочерние image-источники могут одним кликом продвинуть текущий запущенный тег родителя.", "previews": { "title": "Превью-окружения", diff --git a/web/src/routes/apps/[id]/+page.svelte b/web/src/routes/apps/[id]/+page.svelte index cf829e0..a1a9c9b 100644 --- a/web/src/routes/apps/[id]/+page.svelte +++ b/web/src/routes/apps/[id]/+page.svelte @@ -28,6 +28,7 @@ } from '$lib/components/icons'; import ForgeHero from '$lib/components/ForgeHero.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; + import { toasts } from '$lib/stores/toast'; import EventLogEntryComponent from '$lib/components/EventLogEntry.svelte'; import ContainerLogs from '$lib/components/ContainerLogs.svelte'; import ContainerStats from '$lib/components/ContainerStats.svelte'; @@ -405,7 +406,15 @@ // ── Chain (parent / self / children) ────────────────────── let chain = $state(null); let chainError = $state(''); - let promoting = $state(null); // sourceID we're promoting FROM, for UI lock + let promoting = $state(null); // active targetID, for UI lock + // Pending promotion awaiting confirmation. Promote copies the source's + // running image tag onto the target and deploys it, so it's confirmed first. + let confirmPromote = $state<{ + targetID: string; + sourceID: string; + targetName: string; + sourceName: string; + } | null>(null); // ── Branch preview environments ─────────────────────────── // Preview children are the chain children the backend flagged @@ -1083,14 +1092,27 @@ } } - async function promoteFrom(sourceID: string) { + // Open the confirm dialog for a promotion. Direction is encoded by which + // (target, source) pair is passed: parent→self pulls the parent's version + // into this workload; self→child pushes this workload's version to a child. + function askPromote(targetID: string, sourceID: string, targetName: string, sourceName: string) { + confirmPromote = { targetID, sourceID, targetName, sourceName }; + } + + async function doPromote() { + const p = confirmPromote; + confirmPromote = null; + if (!p) return; chainError = ''; - promoting = sourceID; + promoting = p.targetID; try { - await api.promoteFromWorkload(id, sourceID, { deploy: true }); + const res = await api.promoteFromWorkload(p.targetID, p.sourceID, { deploy: true }); + toasts.success( + $t('apps.detail.chainPromoteOk', { tag: res.promoted_tag, target: p.targetName }) + ); await load(); } catch (e) { - chainError = e instanceof Error ? e.message : 'Promote failed'; + chainError = e instanceof Error ? e.message : $t('apps.detail.chainPromoteFailed'); } finally { promoting = null; } @@ -2671,9 +2693,10 @@ {/if} @@ -2700,17 +2723,30 @@ {$t('apps.detail.chainChildrenLabel')}
{#each chain.children as child (child.id)} - - - {child.name} - {#if child.is_preview} - {$t('apps.detail.previews.tag')} - {/if} - - {child.source_kind} - +
+ + + {child.name} + {#if child.is_preview} + {$t('apps.detail.previews.tag')} + {/if} + + {child.source_kind} + + {#if workload?.source_kind === 'image' && child.source_kind === 'image' && !child.is_preview} + + {/if} +
{/each}
@@ -3283,6 +3319,19 @@ }} /> + (confirmPromote = null)} +/> +