mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
fix: pipeline delete takes only its own triggers and discards draft-only nodes by storage key
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6197cba556
commit
a7a6a05e07
@@ -0,0 +1,52 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DraftService, ScriptService } from '$lib/gen'
|
||||
import { planPipelineDelete } from './pipelineDelete'
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
describe('planPipelineDelete', () => {
|
||||
it("takes only the pipeline's own triggers and addresses draft-only nodes by storage key", async () => {
|
||||
vi.spyOn(ScriptService, 'listRunnables').mockResolvedValue({
|
||||
items: [
|
||||
{ type: 'script', path: 'f/sales/ingest', auto_kind: 'pipeline' },
|
||||
{ type: 'script', path: 'f/sales/report' },
|
||||
{
|
||||
type: 'script',
|
||||
path: 'u/alice/draft_abc',
|
||||
draft_only: true,
|
||||
draft_path: 'f/sales/unpublished',
|
||||
auto_kind: 'pipeline'
|
||||
}
|
||||
]
|
||||
} as any)
|
||||
vi.spyOn(DraftService, 'getOwnDraft').mockResolvedValue(undefined as any)
|
||||
const trigger = (runnable_kind: string, runnable_path: string, path: string) => ({
|
||||
trigger_kind: 'schedule',
|
||||
runnable_kind,
|
||||
runnable_path,
|
||||
path
|
||||
})
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
triggers: [
|
||||
trigger('script', 'f/sales/ingest', 'f/sales/ingest_schedule'),
|
||||
trigger('script', 'f/sales/report', 'f/sales/report_schedule'),
|
||||
trigger('flow', 'f/sales/ingest', 'f/sales/flow_schedule')
|
||||
]
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const plan = await planPipelineDelete('w', 'sales')
|
||||
|
||||
expect(plan.triggers).toEqual([{ kind: 'schedule', path: 'f/sales/ingest_schedule' }])
|
||||
expect(plan.scripts).toEqual([
|
||||
{ path: 'f/sales/ingest', displayPath: 'f/sales/ingest', draftOnly: false },
|
||||
{ path: 'u/alice/draft_abc', displayPath: 'f/sales/unpublished', draftOnly: true }
|
||||
])
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
})
|
||||
@@ -26,7 +26,9 @@ async function listPipelineScripts(workspace: string, folder: string): Promise<R
|
||||
return out
|
||||
}
|
||||
|
||||
async function listNativeTriggers(workspace: string, folder: string) {
|
||||
/** Trigger rows starting one of `scripts`. The folder graph also carries the
|
||||
* triggers of the folder's ordinary scripts and flows, which are not the pipeline's. */
|
||||
async function listNativeTriggers(workspace: string, folder: string, scripts: ReadonlySet<string>) {
|
||||
const res = await fetch(
|
||||
`${OpenAPI.BASE ?? ''}/w/${workspace}/assets/graph?${new URLSearchParams({ folder })}`,
|
||||
{ credentials: 'include' }
|
||||
@@ -35,6 +37,8 @@ async function listNativeTriggers(workspace: string, folder: string) {
|
||||
const graph = (await res.json()) as AssetGraphResponse
|
||||
return graph.triggers.flatMap((t) =>
|
||||
t.trigger_kind !== 'asset' &&
|
||||
t.runnable_kind === 'script' &&
|
||||
scripts.has(t.runnable_path) &&
|
||||
t.trigger_kind !== 'webhook' &&
|
||||
t.trigger_kind !== 'data_upload' &&
|
||||
t.path &&
|
||||
@@ -45,7 +49,9 @@ async function listNativeTriggers(workspace: string, folder: string) {
|
||||
}
|
||||
|
||||
export type PipelineDeletePlan = {
|
||||
scripts: { path: string; draftOnly: boolean }[]
|
||||
/** `path` is what the API addresses (a draft-only node's storage key);
|
||||
* `displayPath` is the path the user typed for it. */
|
||||
scripts: { path: string; displayPath: string; draftOnly: boolean }[]
|
||||
triggers: { kind: NativeTriggerKind; path: string }[]
|
||||
/** Whether the caller has an unsaved pipeline draft for this folder. */
|
||||
hasDraft: boolean
|
||||
@@ -56,9 +62,10 @@ export async function planPipelineDelete(
|
||||
workspace: string,
|
||||
folder: string
|
||||
): Promise<PipelineDeletePlan> {
|
||||
const [scripts, triggers, draft] = await Promise.all([
|
||||
listPipelineScripts(workspace, folder),
|
||||
listNativeTriggers(workspace, folder),
|
||||
const scripts = await listPipelineScripts(workspace, folder)
|
||||
const deployed = new Set(scripts.filter((s) => !s.draft_only).map((s) => s.path))
|
||||
const [triggers, draft] = await Promise.all([
|
||||
listNativeTriggers(workspace, folder, deployed),
|
||||
DraftService.getOwnDraft({
|
||||
workspace,
|
||||
kind: PIPELINE_DRAFT_KIND,
|
||||
@@ -67,7 +74,8 @@ export async function planPipelineDelete(
|
||||
])
|
||||
return {
|
||||
scripts: scripts.map((s) => ({
|
||||
path: s.draft_only ? s.draft_path || s.path : s.path,
|
||||
path: s.path,
|
||||
displayPath: (s.draft_only && s.draft_path) || s.path,
|
||||
draftOnly: !!s.draft_only
|
||||
})),
|
||||
triggers,
|
||||
@@ -110,7 +118,7 @@ export async function deletePipeline(
|
||||
}
|
||||
removed++
|
||||
} catch (e) {
|
||||
fail(s.path, e)
|
||||
fail(s.displayPath, e)
|
||||
}
|
||||
}
|
||||
if (plan.hasDraft) {
|
||||
|
||||
@@ -110,13 +110,13 @@
|
||||
{#if plan.scripts.some((s) => !s.draftOnly)}
|
||||
{@render group(
|
||||
hardDelete ? 'Scripts deleted' : 'Scripts archived',
|
||||
plan.scripts.filter((s) => !s.draftOnly).map((s) => s.path)
|
||||
plan.scripts.filter((s) => !s.draftOnly).map((s) => s.displayPath)
|
||||
)}
|
||||
{/if}
|
||||
{#if plan.scripts.some((s) => s.draftOnly)}
|
||||
{@render group(
|
||||
'Draft scripts discarded',
|
||||
plan.scripts.filter((s) => s.draftOnly).map((s) => s.path)
|
||||
plan.scripts.filter((s) => s.draftOnly).map((s) => s.displayPath)
|
||||
)}
|
||||
{/if}
|
||||
{#if plan.triggers.length}
|
||||
|
||||
Reference in New Issue
Block a user