mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 00:03:11 +00:00
* 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>
265 lines
7.7 KiB
Svelte
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>
|