Files
windmill/frontend/src/lib/components/git_sync/GitPushPreview.svelte
T
Ruben Fiszelandwindmill-internal-app[bot] 6acf185854 feat: add an api base url instance setting for displayed urls (#11628)
* feat: add an api base url instance setting for displayed urls

* feat: show the api base url as cli remote, keep mcp url on origin

* fix: keep push captures on origin and load the api base url on svix page

* fix: reject shell metacharacters in webhook and api base urls

* test: drop the trivial api base url resolver test

* feat: hide the api base url setting behind a toggle

* fix: let the quick setup toggle reveal the api base url field

* chore: update ee-repo-ref to 017710bdd0a10de857f0f40800dde238cdb1ad24

This commit updates the EE repository reference after PR #863 was merged in windmill-ee-private.

Previous ee-repo-ref: f67cd621bd45840e6389e0469a67378864f269a0

New ee-repo-ref: 017710bdd0a10de857f0f40800dde238cdb1ad24

Automated by sync-ee-ref workflow.

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-10-08 22:35:07 +02:00

265 lines
7.7 KiB
Svelte

<!--
@component
Lists what pushing the workspace would commit, and pushes it. The host owns the
buttons — the setup dialog puts them in its own footer, the standalone modal in its
— and drives them through `apply()` and `status()`.
-->
<script lang="ts">
import { untrack } from 'svelte'
import { Alert, Button, CopyButton } from '$lib/components/common'
import { Loader2, RotateCw, XCircle, Terminal, ChevronDown, ChevronUp } from 'lucide-svelte'
import GitDiffPreview from '../GitDiffPreview.svelte'
import { JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import hubPaths from '$lib/hubPaths.json'
import { jobManager } from '$lib/services/JobManager'
import type { SyncResponse, SettingsObject } from '$lib/git-sync'
import { apiBaseUrl } from '$lib/apiBaseUrl.svelte'
interface Props {
gitRepoResourcePath: string
uiState: SettingsObject
onSuccess?: () => void
/** Told when a push starts and ends, so a host that can outlive this component knows a
* push is in flight — this one is destroyed the moment its dialog closes. */
onApplyStateChange?: (applying: boolean) => void
}
let { gitRepoResourcePath, uiState, onSuccess, onApplyStateChange }: Props = $props()
let previewJobId = $state<string | null>(null)
let previewJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
let isPreviewLoading = $state(false)
let previewError = $state('')
let applyJobId = $state<string | null>(null)
let applyJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
let isApplying = $state(false)
let applyError = $state('')
let showCliInstructions = $state(false)
const cliInstructions = $derived(`# Setup (only needed if local folder not initialized yet)
npm install -g windmill-cli
wmill workspace add ${$workspaceStore} ${$workspaceStore} ${apiBaseUrl()}/
wmill init --workspace ${$workspaceStore} --repository ${gitRepoResourcePath}
# Pull workspace content to git repository
wmill sync pull --workspace ${$workspaceStore} --repository ${gitRepoResourcePath}`)
let previewResult = $state<SyncResponse | null>(null)
/** What the host needs to label and enable its buttons. */
export function status() {
return {
previewing: isPreviewLoading,
applying: isApplying,
// Nothing to push is not worth a push job.
canApply:
!!previewResult && !previewError && !isApplying && (previewResult.changes?.length ?? 0) > 0
}
}
export function apply() {
return executeJob(false)
}
// The list of changes is what this is for, so it loads on its own rather than on a button.
$effect(() => {
gitRepoResourcePath
untrack(() => void executeJob(true))
})
async function executeJob(isDryRun: boolean) {
const isPreview = isDryRun
if (isPreview) {
isPreviewLoading = true
previewError = ''
previewResult = null
previewJobId = null
previewJobStatus = undefined
} else {
isApplying = true
onApplyStateChange?.(true)
applyError = ''
applyJobId = null
applyJobStatus = undefined
}
try {
const workspace = $workspaceStore
if (!workspace) return
const payload = {
workspace_id: workspace,
repo_url_resource_path: gitRepoResourcePath,
dry_run: isDryRun,
pull: false,
settings_json: JSON.stringify(uiState)
}
const jobId = await JobService.runScriptByPath({
workspace,
path: hubPaths.gitInitRepo,
requestBody: payload,
skipPreprocessor: true
})
if (isPreview) {
previewJobId = jobId
previewJobStatus = 'running'
} else {
applyJobId = jobId
applyJobStatus = 'running'
}
const result = await jobManager.runWithProgress(() => Promise.resolve(jobId), {
workspace,
timeout: 60000,
timeoutMessage: `${isPreview ? 'Preview' : 'Apply'} job timed out after 60s`,
onProgress: (status) => {
if (isPreview) {
previewJobStatus = status.status
} else {
applyJobStatus = status.status
}
if (status.status === 'failure') {
if (isPreview) {
previewError = status.error || 'Listing the changes failed'
} else {
applyError = status.error || 'Push failed'
}
}
}
})
if (isPreview) {
if (previewJobStatus === 'success') {
previewResult = result as SyncResponse
}
} else {
if (applyJobStatus === 'success') {
onSuccess?.()
}
}
} catch (e) {
const errorMsg = e?.message || 'Operation failed'
if (isPreview) {
previewJobStatus = 'failure'
previewError = errorMsg
} else {
applyJobStatus = 'failure'
applyError = errorMsg
}
} finally {
if (isPreview) {
isPreviewLoading = false
} else {
isApplying = false
onApplyStateChange?.(false)
}
}
}
</script>
<div class="flex flex-col gap-4 h-full">
{#if isPreviewLoading}
<div class="flex-1 min-h-24 flex flex-col items-center justify-center gap-2 text-sm text-hint">
<Loader2 size={36} class="animate-spin" />
Listing the changes to push...
</div>
{:else}
<p class="text-xs text-secondary">
Here is what will get pushed to the repository, according to the filters you set.
</p>
{/if}
{#if previewError}
<Alert type="error" title="Could not list the changes">
<div class="flex flex-col items-start gap-2">
<span>{previewError}</span>
<Button
variant="default"
unifiedSize="xs"
startIcon={{ icon: RotateCw }}
onClick={() => void executeJob(true)}
>
Retry
</Button>
</div>
</Alert>
{/if}
{#if previewResult && !previewError}
<div class="flex flex-col gap-2 flex-1 min-h-0">
<h4 class="text-sm font-semibold text-primary">Changes to push</h4>
{#if previewResult.changes?.length > 0}
<GitDiffPreview {previewResult} heightClass="flex-1 min-h-0" />
{:else}
<div class="text-sm text-secondary">
The repository already matches this workspace: there is nothing to push.
</div>
{/if}
</div>
{/if}
{#if applyError}
<Alert type="error" title="Push failed">
{applyError}
</Alert>
{/if}
<div class="mt-auto flex flex-col gap-2">
{#each [{ id: previewJobId, status: previewJobStatus, label: 'Changes job', running: isPreviewLoading }, { id: applyJobId, status: applyJobStatus, label: 'Push job', running: isApplying }] as job (job.label)}
<!-- The row is held from the moment the job is started, since its id only arrives once
the server has taken it; it goes once the job succeeded, its outcome being on
screen already. -->
{#if job.running || (job.id && job.status !== 'success')}
<div class="flex items-center gap-2 min-h-4 text-2xs text-secondary">
{#if job.status === 'running'}
<Loader2 class="animate-spin" size={12} />
{:else if job.status === 'failure'}
<XCircle size={12} class="text-red-700" />
{/if}
{#if job.id}
<span class="text-hint">{job.label}:</span>
<a
target="_blank"
class="underline"
href={`/run/${job.id}?workspace=${$workspaceStore}`}
>
{job.id}
</a>
{/if}
</div>
{/if}
{/each}
<!-- Nothing to copy commands for until the changes are known. -->
<div class={isPreviewLoading ? 'hidden' : ''}>
<div class="flex">
<Button
variant="subtle"
unifiedSize="xs"
startIcon={{ icon: Terminal }}
endIcon={{ icon: showCliInstructions ? ChevronUp : ChevronDown }}
onClick={() => (showCliInstructions = !showCliInstructions)}
>
CLI instructions
</Button>
</div>
{#if showCliInstructions}
<div class="relative mt-2">
<CopyButton value={cliInstructions} class="absolute top-1 right-1" />
<pre
class="text-xs bg-surface-sunken rounded-lg p-3 pr-10 overflow-x-auto whitespace-pre-wrap break-all"
>{cliInstructions}</pre
>
</div>
{/if}
</div>
</div>
</div>