mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 00:06:14 +00:00
feat: keep a Hub project live while an update is under review (#10814)
* feat: keep a Hub project live while an update is under review Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: confirm before discarding a Hub update and document the route Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: bind the discard confirmation to the session that opened it Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: keep the old wording against a Hub without pending updates Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: say what the review lock actually blocks, in one alert Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * feat: let a publisher cancel a Hub submission from the wizard Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: hide the cancel action on a Hub that cannot withdraw Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * docs: describe startNewDraft for both Hub versions Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * feat: warn when an update carries the published pipeline replay Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: base the stale-replay warning on changed content, not recordings Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: stop the stale-replay warning leaking across updates Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: clear the captured cascade when starting another update Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d * fix: abandon an in-flight cascade when starting another update Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018At6NKGa6cQP1zakMS686d --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25287,6 +25287,68 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/hub/projects/{slug}/withdraw:
|
||||
post:
|
||||
summary: take a hub project submission back out of review
|
||||
description: |
|
||||
Requires the caller to be a workspace admin. Forwards the request to the
|
||||
configured Hub scoped to the `{workspace}:{folder}` source and returns
|
||||
the Hub's status code and raw response body. Everything pushed for the
|
||||
submission is kept, so it can be fixed and submitted again.
|
||||
operationId: withdrawHubProject
|
||||
tags:
|
||||
- hubPublish
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: slug
|
||||
in: path
|
||||
required: true
|
||||
description: hub project slug (3-50 chars, lowercase alphanumeric and hyphens, no leading/trailing hyphen)
|
||||
schema:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 50
|
||||
pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$"
|
||||
- $ref: "#/components/parameters/HubPublishFolder"
|
||||
responses:
|
||||
"200":
|
||||
description: raw Hub response body (status code is passed through from the Hub)
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/hub/projects/{slug}/discard_update:
|
||||
post:
|
||||
summary: discard the pending update to a published hub project
|
||||
description: |
|
||||
Requires the caller to be a workspace admin. Forwards the request to the
|
||||
configured Hub scoped to the `{workspace}:{folder}` source and returns
|
||||
the Hub's status code and raw response body. The published project is
|
||||
left untouched.
|
||||
operationId: discardHubProjectUpdate
|
||||
tags:
|
||||
- hubPublish
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: slug
|
||||
in: path
|
||||
required: true
|
||||
description: hub project slug (3-50 chars, lowercase alphanumeric and hyphens, no leading/trailing hyphen)
|
||||
schema:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 50
|
||||
pattern: "^[a-z0-9][a-z0-9-]*[a-z0-9]$"
|
||||
- $ref: "#/components/parameters/HubPublishFolder"
|
||||
responses:
|
||||
"200":
|
||||
description: raw Hub response body (status code is passed through from the Hub)
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/hub/project:
|
||||
get:
|
||||
summary: get the hub project linked to a workspace folder
|
||||
|
||||
@@ -42,6 +42,11 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/migrations", post(publish_migrations))
|
||||
.route("/projects/{slug}/export", get(get_project_export))
|
||||
.route("/projects/{slug}/submit", post(submit_project))
|
||||
.route("/projects/{slug}/withdraw", post(withdraw_project))
|
||||
.route(
|
||||
"/projects/{slug}/discard_update",
|
||||
post(discard_project_update),
|
||||
)
|
||||
.route("/project", get(get_project_by_source))
|
||||
}
|
||||
|
||||
@@ -554,9 +559,35 @@ async fn submit_project(
|
||||
.await
|
||||
}
|
||||
|
||||
// The Hub has no auth of its own: it validates bearer tokens by calling this
|
||||
// instance's /api/users/whoami. Forwarding the caller's own token logs them in
|
||||
// on the Hub as themselves (account auto-created on first use).
|
||||
// Take a submission back out of review, keeping what was pushed for it.
|
||||
async fn withdraw_project(
|
||||
ctx: HubPublishCtx,
|
||||
Path((_workspace, slug)): Path<(String, ProjectSlug)>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
ctx.post(
|
||||
&format!("/projects/{}/withdraw", slug),
|
||||
&serde_json::json!({}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// Throw away the pending update to an already-published project. The published
|
||||
// version is untouched — it never saw the update.
|
||||
async fn discard_project_update(
|
||||
ctx: HubPublishCtx,
|
||||
Path((_workspace, slug)): Path<(String, ProjectSlug)>,
|
||||
) -> Result<impl IntoResponse, Error> {
|
||||
ctx.post(
|
||||
&format!("/projects/{}/discard_update", slug),
|
||||
&serde_json::json!({}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// The Hub has no auth of its own: it validates bearer tokens by calling
|
||||
// /api/users/whoami — on app.windmill.dev for the public Hub, on the paired
|
||||
// instance for a private one. Forwarding the caller's own token logs them in on
|
||||
// the Hub as themselves (account auto-created on first use).
|
||||
async fn get_from_hub(
|
||||
path: &str,
|
||||
source_id: &str,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import { Alert, Badge, Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import WorkspaceDeployLayout from '$lib/components/WorkspaceDeployLayout.svelte'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
@@ -13,7 +13,8 @@
|
||||
canRecordSession,
|
||||
sanitizeSlug,
|
||||
isValidSlug,
|
||||
type DeployItem
|
||||
type DeployItem,
|
||||
type DeployToHubSession
|
||||
} from './deployToHubSession.svelte'
|
||||
import { TRIGGER_KINDS, triggerDetails } from '$lib/components/triggers/workspaceTriggersList'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
@@ -40,6 +41,7 @@
|
||||
Zap
|
||||
} from 'lucide-svelte'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
|
||||
// Folder name (no `f/`) this project is scoped to; provided by the /folders launcher.
|
||||
@@ -61,6 +63,12 @@
|
||||
// Inline pipeline graph above the item list, collapsed by default so the
|
||||
// selection list stays the first thing in view.
|
||||
let pipelineGraphOpen = $state(false)
|
||||
// Discarding throws away every item pushed for the update and any recording made
|
||||
// for it, none of which can be recovered. The session that opened the dialog is
|
||||
// held rather than a boolean: a workspace or folder switch (browser history, say)
|
||||
// replaces the session underneath an open dialog, and confirming must never
|
||||
// discard a different folder's update.
|
||||
let discardTarget = $state<DeployToHubSession | undefined>(undefined)
|
||||
let resourceDrawer = $state<Drawer | undefined>()
|
||||
let triggerDrawer = $state<Drawer | undefined>()
|
||||
let bundleDrawer = $state<Drawer | undefined>()
|
||||
@@ -146,6 +154,22 @@
|
||||
</script>
|
||||
|
||||
{#if deployHub.session}
|
||||
<ConfirmationModal
|
||||
open={discardTarget !== undefined}
|
||||
title="Discard update"
|
||||
confirmationText="Discard"
|
||||
onConfirmed={async () => {
|
||||
const target = discardTarget
|
||||
discardTarget = undefined
|
||||
if (target && target === deployHub.session) await target.discardUpdate()
|
||||
}}
|
||||
onCanceled={() => (discardTarget = undefined)}
|
||||
>
|
||||
<span>
|
||||
Discard this update? Everything pushed for it, including recordings made for it, is deleted
|
||||
and cannot be recovered. Your published project is unaffected.
|
||||
</span>
|
||||
</ConfirmationModal>
|
||||
{#key deployHub.session}
|
||||
{@const s = deployHub.session}
|
||||
<div>
|
||||
@@ -178,8 +202,10 @@
|
||||
</span>
|
||||
<li class={stepNum === 1 ? 'text-primary' : stepNum > 1 ? 'opacity-60' : ''}>
|
||||
<span class="font-mono text-emphasis">{stepNum > 1 ? '✓' : '1.'}</span>
|
||||
<span class="font-semibold text-primary">Bundle your project</span> — creates a draft
|
||||
on the Hub with every selected script, flow, app and resource from this folder.
|
||||
<span class="font-semibold text-primary">Bundle your project</span> — sends every
|
||||
selected script, flow, app and resource from this folder to the Hub{s.liveOnHub
|
||||
? ' as an update'
|
||||
: ' as a draft'}.
|
||||
</li>
|
||||
<li
|
||||
class={stepNum === 2 ? 'text-primary' : stepNum > 2 ? 'opacity-60' : 'opacity-40'}
|
||||
@@ -235,7 +261,7 @@
|
||||
startIcon={{ icon: Cloud }}
|
||||
onclick={openBundle}
|
||||
>
|
||||
Create Hub draft ({s.selectedItems.length})
|
||||
{s.liveOnHub ? 'Bundle update' : 'Create Hub draft'} ({s.selectedItems.length})
|
||||
</Button>
|
||||
{:else if s.phase === 'draft'}
|
||||
<Button
|
||||
@@ -247,6 +273,17 @@
|
||||
Submit for review
|
||||
</Button>
|
||||
{:else if s.phase === 'under_review'}
|
||||
{#if s.hubSupportsUpdates}
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="sm"
|
||||
loading={s.withdrawing}
|
||||
startIcon={{ icon: X }}
|
||||
onclick={s.cancelSubmission}
|
||||
>
|
||||
Cancel submission
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
@@ -262,7 +299,19 @@
|
||||
startIcon={{ icon: RotateCcw }}
|
||||
onclick={s.startNewDraft}
|
||||
>
|
||||
New draft
|
||||
{s.liveOnHub ? 'Publish an update' : 'New draft'}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if s.liveOnHub && s.phase === 'draft'}
|
||||
<Button
|
||||
variant="default"
|
||||
destructive
|
||||
unifiedSize="sm"
|
||||
loading={s.discardingUpdate}
|
||||
startIcon={{ icon: X }}
|
||||
onclick={() => (discardTarget = s)}
|
||||
>
|
||||
Discard update
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -270,8 +319,10 @@
|
||||
{#if s.phase === 'predeploy'}
|
||||
<div class="flex flex-col gap-1 pb-3">
|
||||
<span class="text-xs text-secondary">
|
||||
Bundling creates a draft project on the Hub from the selected scripts, flows and
|
||||
apps of <span class="font-mono">{s.selectedFolder}/</span>.
|
||||
Bundling creates {s.liveOnHub
|
||||
? 'an update to your Hub project'
|
||||
: 'a draft project on the Hub'} from the selected scripts, flows and apps of
|
||||
<span class="font-mono">{s.selectedFolder}/</span>.
|
||||
{s.selectedItems.length} of {s.filteredWorkspaceItems.length} items selected.
|
||||
</span>
|
||||
</div>
|
||||
@@ -351,6 +402,31 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if s.liveOnHub && s.phase !== 'live' && s.phase !== 'under_review'}
|
||||
<Alert
|
||||
type="info"
|
||||
size="xs"
|
||||
title={s.phase === 'predeploy'
|
||||
? 'Your published project stays live'
|
||||
: 'This is an update — your published project is still live'}
|
||||
>
|
||||
Visitors keep seeing the published version, with its stars, forks and comments,
|
||||
until this update is approved. Approving replaces it in place; discarding leaves it
|
||||
exactly as it is.
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if s.pipelineReplayMayBeStale && s.phase === 'draft'}
|
||||
<Alert type="warning" size="xs" title="The data pipeline replay is the published one">
|
||||
This update carries the cascade recorded for the version that is live, and at least
|
||||
one item has changed since. Record it again below, or visitors will replay the old
|
||||
run as though it were this version.
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if s.rejectionReason && s.phase === 'draft'}
|
||||
<Alert type="error" size="xs" title="Changes requested">
|
||||
{s.rejectionReason}
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if s.phase === 'draft'}
|
||||
<div class="flex flex-col gap-1 pb-3">
|
||||
<span class="text-xs text-secondary">
|
||||
@@ -477,18 +553,21 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if s.phase === 'under_review'}
|
||||
<div
|
||||
class="flex items-start gap-2 rounded-md border border-blue-300 bg-blue-50 p-3 text-xs text-blue-900 dark:border-blue-800 dark:bg-blue-950/40 dark:text-blue-100"
|
||||
<Alert
|
||||
type="info"
|
||||
size="xs"
|
||||
title={s.liveOnHub
|
||||
? 'Update under review — your published project is still live'
|
||||
: 'Under review'}
|
||||
>
|
||||
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="font-semibold">Locked while under review</span>
|
||||
<span>
|
||||
The Windmill team is reviewing this submission. Editing, recording, and sharing
|
||||
actions are disabled. Estimated turnaround: 1-2 business days.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
The Windmill team is reviewing your project. Submission is locked until they answer
|
||||
— no new version can be sent to the Hub, and no recording added to this one.
|
||||
Estimated turnaround: 1-2 business days{#if s.hubSupportsUpdates}; cancel the
|
||||
submission to get back to it sooner{/if}.{#if s.liveOnHub}
|
||||
Visitors keep seeing the published version meanwhile, with its stars, forks and
|
||||
comments; approving replaces it in place.{/if} Your folder itself is untouched — keep
|
||||
editing your scripts and flows as usual.
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if s.phase === 'draft'}
|
||||
{@const recordedCount = s.recordableItems.filter((i) => i.rec === 'recorded').length}
|
||||
@@ -668,7 +747,11 @@
|
||||
Waiting for the Windmill team to review the submission.
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-[11px] text-hint"> Iterate further by starting a new draft. </span>
|
||||
<span class="text-[11px] text-hint">
|
||||
{s.liveOnHub
|
||||
? 'Publish an update to change it — this stays live until the update is approved.'
|
||||
: 'Iterate further by starting a new draft.'}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -198,8 +198,31 @@ export class DeployToHubSession {
|
||||
// Whether the Hub currently has a custom logo for this project (from
|
||||
// rehydration) — drives the "Remove current logo" affordance.
|
||||
hubHasRemoteLogo = $state(false)
|
||||
// A pipeline recording is attached on the Hub. An update inherits the published
|
||||
// one, which is only a demo of the new version if nothing it runs changed.
|
||||
hubHasPipelineRecording = $state(false)
|
||||
// The Hub's own verdict: this update runs different content from the published
|
||||
// version. False when there is no update in flight.
|
||||
hubItemsChanged = $state(false)
|
||||
// The attached pipeline recording is the published version's, copied when this
|
||||
// update started, rather than one recorded for it. Authoritative across reloads,
|
||||
// unlike `pipelineRecorded`, which only remembers this session.
|
||||
hubPipelineRecordingInherited = $state(false)
|
||||
effectiveSlug = $state('')
|
||||
hubItemIds = $state<Record<string, number>>({})
|
||||
// Set once the project is published: everything the wizard shows from here on
|
||||
// describes an update to it, and the published version keeps serving until that
|
||||
// update is approved. `phase` is the update's own status, not the project's.
|
||||
liveOnHub = $state(false)
|
||||
/** This Hub knows about pending updates — it answers rehydration with a `live`
|
||||
* key. An older one takes a project offline to republish and has neither the
|
||||
* withdraw nor the discard endpoint, so the actions built on them stay hidden. */
|
||||
hubSupportsUpdates = $state(false)
|
||||
// A reviewer's verdict on the current draft, shown so the publisher knows what
|
||||
// to fix before resubmitting.
|
||||
rejectionReason = $state<string | undefined>(undefined)
|
||||
discardingUpdate = $state(false)
|
||||
withdrawing = $state(false)
|
||||
|
||||
// Best-effort data table migrations for the bundle, editable in the drawer and
|
||||
// pushed on deploy. Regenerated when the bundle drawer opens.
|
||||
@@ -228,6 +251,10 @@ export class DeployToHubSession {
|
||||
submitting = $state(false)
|
||||
syncing = $state(false)
|
||||
|
||||
// Set from the Hub's answer to the draft request: this push went into an update
|
||||
// rather than over the published project.
|
||||
#publishedAsUpdate = false
|
||||
|
||||
// Intra-session tokens: latest call wins among competing calls on this session.
|
||||
#triggerLoadTok = 0
|
||||
#recordRunTok = 0
|
||||
@@ -316,6 +343,18 @@ export class DeployToHubSession {
|
||||
)
|
||||
pipelineScriptPathSet = $derived(new Set(this.pipelineScriptPaths))
|
||||
isPipelineProject = $derived(this.pipelineScriptPaths.length > 0)
|
||||
/** The pipeline replay this update carries came from the published version, and
|
||||
* something it runs has changed since — so it is a recording of another version.
|
||||
* `hubItemsChanged` is the Hub comparing content, not a guess from which items
|
||||
* carry recordings: an item nobody ever recorded has not changed. */
|
||||
pipelineReplayMayBeStale = $derived(
|
||||
this.liveOnHub &&
|
||||
this.isPipelineProject &&
|
||||
this.hubHasPipelineRecording &&
|
||||
this.hubPipelineRecordingInherited &&
|
||||
this.hubItemsChanged &&
|
||||
!this.pipelineRecorded
|
||||
)
|
||||
hubSlug = $derived(this.effectiveSlug || sanitizeSlug(this.hubName))
|
||||
|
||||
relevantTriggers = $derived.by(() => {
|
||||
@@ -573,6 +612,17 @@ export class DeployToHubSession {
|
||||
this.hubSummary = p.summary ?? ''
|
||||
this.hubReadme = p.readme ?? ''
|
||||
this.hubHasRemoteLogo = p.has_logo === true
|
||||
this.hubHasPipelineRecording = p.has_pipeline_recording === true
|
||||
this.hubItemsChanged = p.items_changed === true
|
||||
this.hubPipelineRecordingInherited = p.pipeline_recording_inherited === true
|
||||
this.rejectionReason = p.rejection_reason ?? undefined
|
||||
// `live` is a key this Hub always sends — null unless an update is in
|
||||
// flight, in which case the fields above describe that update and the
|
||||
// project itself is still published. Its absence means a Hub old enough to
|
||||
// still take a project offline while it re-publishes, so the wizard must
|
||||
// not promise otherwise.
|
||||
this.hubSupportsUpdates = 'live' in p
|
||||
this.liveOnHub = this.hubSupportsUpdates && (p.live?.approved === true || p.status === 'live')
|
||||
this.phase =
|
||||
p.status === 'live' ? 'live' : p.status === 'under_review' ? 'under_review' : 'draft'
|
||||
const ids: Record<string, number> = {}
|
||||
@@ -898,6 +948,9 @@ export class DeployToHubSession {
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
if (typeof parsed?.slug === 'string') returnedSlug = parsed.slug
|
||||
// The Hub decides this: publishing over an approved project goes into a
|
||||
// pending update instead, and the project keeps serving meanwhile.
|
||||
this.#publishedAsUpdate = parsed?.pending_revision === true
|
||||
} catch {}
|
||||
if (!returnedSlug) {
|
||||
sendUserToast(`Hub did not return a slug. Aborting publish to avoid path drift.`, true)
|
||||
@@ -1252,8 +1305,13 @@ export class DeployToHubSession {
|
||||
// UI stuck in `predeploy`; rehydrate then upgrades to authoritative state.
|
||||
this.draftItems = itemsSnapshot.map((i) => ({ ...i, rec: 'none' }))
|
||||
this.phase = 'draft'
|
||||
const asUpdate = this.#publishedAsUpdate
|
||||
await this.rehydrateFromHub()
|
||||
sendUserToast(`Draft created on the Hub. Add recordings before submitting for review.`)
|
||||
sendUserToast(
|
||||
asUpdate
|
||||
? `Update ready on the Hub. Your published project stays live until it is approved.`
|
||||
: `Draft created on the Hub. Add recordings before submitting for review.`
|
||||
)
|
||||
} finally {
|
||||
this.deploying = false
|
||||
}
|
||||
@@ -1313,12 +1371,82 @@ export class DeployToHubSession {
|
||||
}
|
||||
}
|
||||
|
||||
/** Go back to picking items, to publish again. Local only — nothing reaches the
|
||||
* Hub until the bundle is confirmed, and where the Hub supports updates the
|
||||
* published version keeps serving even then. */
|
||||
startNewDraft = () => {
|
||||
this.draftItems = []
|
||||
this.recordings = {}
|
||||
this.rejectionReason = undefined
|
||||
// All of it belongs to the update just finished, not the one starting. The
|
||||
// captured cascade especially: left in place, the next update could save a
|
||||
// replay of the version it replaces. Bumping the token first abandons a run
|
||||
// still in flight, which would otherwise write its result back over this.
|
||||
this.#pipelineRunTok++
|
||||
this.pipelineRecorded = false
|
||||
this.pipelineRecordingResult = undefined
|
||||
this.pipelineRunState = 'idle'
|
||||
this.pipelineRunError = undefined
|
||||
this.phase = 'predeploy'
|
||||
}
|
||||
|
||||
/** Take the submission back out of review. Everything pushed for it is kept, so
|
||||
* it can be fixed and submitted again. */
|
||||
cancelSubmission = async () => {
|
||||
if (this.withdrawing) return
|
||||
const slug = this.effectiveSlug
|
||||
if (!slug) return
|
||||
this.withdrawing = true
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/w/${this.workspace}/hub/projects/${encodeURIComponent(slug)}/withdraw${this.#folderQs()}`,
|
||||
{ method: 'POST', credentials: 'include' }
|
||||
)
|
||||
if (!res.ok) {
|
||||
sendUserToast(`Could not cancel the submission: ${await res.text()}`, true)
|
||||
return
|
||||
}
|
||||
if (this.#disposed) return
|
||||
this.phase = 'draft'
|
||||
await this.rehydrateFromHub()
|
||||
sendUserToast(`Submission cancelled. Everything you pushed is still here.`)
|
||||
} catch (e: any) {
|
||||
sendUserToast(`Could not cancel the submission: ${e?.message ?? e}`, true)
|
||||
} finally {
|
||||
this.withdrawing = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Throw away an update in progress and go back to what is published. */
|
||||
discardUpdate = async () => {
|
||||
if (this.discardingUpdate) return
|
||||
const slug = this.effectiveSlug
|
||||
if (!slug) return
|
||||
this.discardingUpdate = true
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/w/${this.workspace}/hub/projects/${encodeURIComponent(slug)}/discard_update${this.#folderQs()}`,
|
||||
{ method: 'POST', credentials: 'include' }
|
||||
)
|
||||
if (!res.ok) {
|
||||
sendUserToast(`Could not discard the update: ${await res.text()}`, true)
|
||||
return
|
||||
}
|
||||
if (this.#disposed) return
|
||||
this.draftItems = []
|
||||
this.recordings = {}
|
||||
this.deploymentStatus = {}
|
||||
this.rejectionReason = undefined
|
||||
this.phase = 'live'
|
||||
await this.rehydrateFromHub()
|
||||
sendUserToast(`Update discarded. The published project is unchanged.`)
|
||||
} catch (e: any) {
|
||||
sendUserToast(`Could not discard the update: ${e?.message ?? e}`, true)
|
||||
} finally {
|
||||
this.discardingUpdate = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset record-drawer state and load the target's schema. */
|
||||
async openRecord(it: DeployItem) {
|
||||
const tok = ++this.#recordRunTok
|
||||
|
||||
Reference in New Issue
Block a user