mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
feat: delete a node's script, trigger, or both from its menu; red header for a script with errors
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
2d72eeb765
commit
ea9bc2119e
@@ -1462,9 +1462,9 @@
|
||||
foldedIds,
|
||||
...(toDelete
|
||||
? {
|
||||
deleteCount: toDelete.trigger ? 2 : 1,
|
||||
upstreamDelete: toDelete,
|
||||
deleteVerb: assetDeleteVerb,
|
||||
onDeleteUpstream: () => onDeleteAssetUpstream?.(toDelete)
|
||||
onDeleteUpstream: (t: AssetUpstreamDelete) => onDeleteAssetUpstream?.(t)
|
||||
}
|
||||
: onDeleteAssetUpstream && u
|
||||
? {
|
||||
@@ -1705,8 +1705,13 @@
|
||||
.map((id) => toFix(nodeFixes?.get(id)))
|
||||
.filter((f) => f != undefined)
|
||||
const scriptsToDrag = onAddAssetTrigger ? dragScripts(n) : []
|
||||
// Assets-only: a folded script with an error reddens the header naming it.
|
||||
const scriptError = ((n.data.foldedIds ?? []) as string[]).some(
|
||||
(id) => id.startsWith('script:') && nodeFixes?.has(id)
|
||||
)
|
||||
const data = {
|
||||
...withUpstream,
|
||||
...(scriptError ? { scriptError } : {}),
|
||||
...(fix ? { fix } : {}),
|
||||
...(foldedFixes.length ? { foldedFixes } : {}),
|
||||
...(scriptsToDrag.length
|
||||
|
||||
@@ -171,14 +171,16 @@
|
||||
noAssetName?: string
|
||||
/** Assets-only: the node is only its script's banner. */
|
||||
bannerOnly?: boolean
|
||||
/** Assets-only: a script folded into the node has an error. */
|
||||
scriptError?: boolean
|
||||
/** No script in the workspace writes it; a flow's write fires no asset trigger. */
|
||||
neverWritten?: boolean
|
||||
/** Card width in the assets-only view, sized to its text. */
|
||||
width?: number
|
||||
/** Assets-only view: deletes the producing script and trigger no other
|
||||
* asset shares; `deleteCount` is how many that is. */
|
||||
onDeleteUpstream?: () => void
|
||||
deleteCount?: number
|
||||
/** Assets-only view: the producing script and trigger no other asset
|
||||
* shares, which the menu offers to delete one by one or together. */
|
||||
upstreamDelete?: import('./assetsOnlyView').AssetUpstreamDelete
|
||||
onDeleteUpstream?: (target: import('./assetsOnlyView').AssetUpstreamDelete) => void
|
||||
deleteVerb?: 'Delete' | 'Archive'
|
||||
/** Why nothing can be deleted, when the producer is shared. */
|
||||
deleteBlocked?: string
|
||||
@@ -229,15 +231,8 @@
|
||||
// and a step downstream of what nothing writes would never run on its writes.
|
||||
let showAdd = $derived(data.onAddScript != undefined && !data.error && !data.neverWritten)
|
||||
let menuItems: Item[] = $derived(
|
||||
data.onDeleteUpstream
|
||||
? [
|
||||
{
|
||||
displayName: `${data.deleteVerb ?? 'Delete'} ${data.deleteCount} item${data.deleteCount === 1 ? '' : 's'}`,
|
||||
icon: Trash2,
|
||||
type: 'delete' as const,
|
||||
action: () => data.onDeleteUpstream?.()
|
||||
}
|
||||
]
|
||||
data.onDeleteUpstream && data.upstreamDelete
|
||||
? deleteItems(data.upstreamDelete, data.onDeleteUpstream)
|
||||
: data.deleteBlocked
|
||||
? [
|
||||
{
|
||||
@@ -248,6 +243,42 @@
|
||||
]
|
||||
: []
|
||||
)
|
||||
const lastSegment = (p: string) => p.split('/').pop() ?? p
|
||||
// A script is archived rather than deleted when the user may not delete it;
|
||||
// a trigger is always deleted.
|
||||
function deleteItems(
|
||||
t: import('./assetsOnlyView').AssetUpstreamDelete,
|
||||
run: (target: import('./assetsOnlyView').AssetUpstreamDelete) => void
|
||||
): Item[] {
|
||||
const items: Item[] = []
|
||||
if (t.script) {
|
||||
const script = t.script
|
||||
items.push({
|
||||
displayName: `${data.deleteVerb ?? 'Delete'} script ${lastSegment(script.path)}`,
|
||||
icon: Trash2,
|
||||
type: 'delete' as const,
|
||||
action: () => run({ script })
|
||||
})
|
||||
}
|
||||
if (t.trigger) {
|
||||
const trigger = t.trigger
|
||||
items.push({
|
||||
displayName: `Delete ${trigger.kind} trigger ${lastSegment(trigger.path)}`,
|
||||
icon: Trash2,
|
||||
type: 'delete' as const,
|
||||
action: () => run({ trigger })
|
||||
})
|
||||
}
|
||||
if (t.script && t.trigger) {
|
||||
items.push({
|
||||
displayName: `${data.deleteVerb === 'Archive' ? 'Remove' : 'Delete'} all 2 items`,
|
||||
icon: Trash2,
|
||||
type: 'delete' as const,
|
||||
action: () => run(t)
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
let upstream = $derived(data.upstream)
|
||||
const HEADER_SELECTED = 'bg-surface-accent-selected hover:bg-surface-accent-selected text-accent'
|
||||
const CHIP_SELECTED =
|
||||
@@ -335,7 +366,9 @@
|
||||
naming it, which opens it. Several scripts open a menu to pick one. -->
|
||||
{@const one = producer ?? (coProducers.length === 1 ? coProducers[0] : undefined)}
|
||||
{@const headerClass = twMerge(
|
||||
'flex items-center gap-1.5 w-full min-w-0 px-2 text-3xs font-normal leading-none text-secondary hover:bg-surface-hover hover:text-primary'
|
||||
'flex items-center gap-1.5 w-full min-w-0 px-2 text-3xs font-normal leading-none text-secondary hover:bg-surface-hover hover:text-primary',
|
||||
data.scriptError &&
|
||||
'bg-red-100 dark:bg-red-700 text-red-700 dark:text-red-200 hover:bg-red-200 dark:hover:bg-red-600 hover:text-red-800'
|
||||
)}
|
||||
{#if one}
|
||||
{@const isSelected = producer ? !!producer.selected : coSelected}
|
||||
@@ -501,6 +534,7 @@
|
||||
? upstreamRow
|
||||
: undefined}
|
||||
header={hasHeader ? scriptHeader : undefined}
|
||||
headerTone={data.scriptError ? 'error' : undefined}
|
||||
headerSelected={producer ? !!producer.selected : coSelected}
|
||||
width={data.width}
|
||||
surface={upstream ? 'primary' : 'secondary'}
|
||||
|
||||
@@ -44,6 +44,8 @@ and the controls hung around the card.
|
||||
header?: Snippet
|
||||
/** The header's script is selected: its edge takes the selected colour. */
|
||||
headerSelected?: boolean
|
||||
/** The header's script has an error: its edge goes red. */
|
||||
headerTone?: 'error'
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -61,6 +63,7 @@ and the controls hung around the card.
|
||||
subtitle,
|
||||
header,
|
||||
headerSelected = false,
|
||||
headerTone,
|
||||
width = NODE.width
|
||||
}: Props = $props()
|
||||
|
||||
@@ -137,6 +140,7 @@ and the controls hung around the card.
|
||||
<div
|
||||
class={twMerge(
|
||||
'mx-2 flex items-stretch min-w-0 rounded-t-md overflow-hidden bg-surface border border-b-0 border-gray-200 dark:border-gray-700',
|
||||
headerTone === 'error' && 'border-red-300 dark:border-red-600',
|
||||
headerSelected && 'border-border-selected dark:border-border-selected'
|
||||
)}
|
||||
style="height: {PIPELINE_NODE_HEADER}px;"
|
||||
|
||||
@@ -20,8 +20,9 @@ export const noAssetNodeId = (runnableId: string) => `no-asset:${runnableId}`
|
||||
export const isNoAssetNode = (id: string) => id.startsWith('no-asset:')
|
||||
|
||||
/** The script and trigger an assets-only asset's delete removes. */
|
||||
/** What an assets-only delete removes: the script, its trigger, or both. */
|
||||
export type AssetUpstreamDelete = {
|
||||
script: { path: string; unsaved: boolean }
|
||||
script?: { path: string; unsaved: boolean }
|
||||
trigger?: { kind: NativeTriggerKind; path: string; draft: boolean }
|
||||
}
|
||||
|
||||
|
||||
@@ -1282,26 +1282,12 @@
|
||||
if (!target || !ws) return
|
||||
assetDeleteLoading = true
|
||||
const { script, trigger } = target
|
||||
const n = (script ? 1 : 0) + (trigger ? 1 : 0)
|
||||
try {
|
||||
// The script first: when it is refused, nothing else has changed.
|
||||
const deployed = graphRes.current?.runnables.some(
|
||||
(r) => r.path === script.path && !r.unsaved
|
||||
)
|
||||
if (deployed) {
|
||||
if (canHardDeleteScripts) {
|
||||
await ScriptService.deleteScriptByPath({ workspace: ws, path: script.path })
|
||||
} else {
|
||||
await ScriptService.archiveScriptByPath({ workspace: ws, path: script.path })
|
||||
}
|
||||
}
|
||||
if (pe.drafts.has(script.path)) discardDraft(script.path)
|
||||
if (deployed) {
|
||||
pe.discardTriggerDraftsFor(script.path)
|
||||
forgetPath(script.path)
|
||||
forgetScriptCache(script.path)
|
||||
}
|
||||
if (script) await removeScript(script, ws)
|
||||
} catch (e: any) {
|
||||
sendUserToast(`Could not remove ${script.path}: ${e?.body ?? e?.message ?? String(e)}`, true)
|
||||
sendUserToast(`Could not remove ${script?.path}: ${e?.body ?? e?.message ?? String(e)}`, true)
|
||||
assetDeleteLoading = false
|
||||
return
|
||||
}
|
||||
@@ -1311,11 +1297,10 @@
|
||||
if (pe.triggerDrafts.has(key)) pe.discardTriggerDraft(key)
|
||||
if (!trigger.draft) await deleteTriggerRow(trigger.kind, trigger.path, ws)
|
||||
}
|
||||
const n = trigger ? 2 : 1
|
||||
sendUserToast(`Removed ${n} item${n === 1 ? '' : 's'}`)
|
||||
} catch (e: any) {
|
||||
sendUserToast(
|
||||
`Removed ${script.path}, but not its ${trigger?.kind} trigger ${trigger?.path}: ${e?.body ?? e?.message ?? String(e)}`,
|
||||
`${script ? `Removed ${script.path}, but not its` : 'Could not remove the'} ${trigger?.kind} trigger ${trigger?.path}: ${e?.body ?? e?.message ?? String(e)}`,
|
||||
true
|
||||
)
|
||||
} finally {
|
||||
@@ -1324,6 +1309,24 @@
|
||||
await graphRes.refetch()
|
||||
}
|
||||
}
|
||||
async function removeScript(script: { path: string }, ws: string) {
|
||||
const deployed = graphRes.current?.runnables.some(
|
||||
(r) => r.path === script.path && !r.unsaved
|
||||
)
|
||||
if (deployed) {
|
||||
if (canHardDeleteScripts) {
|
||||
await ScriptService.deleteScriptByPath({ workspace: ws, path: script.path })
|
||||
} else {
|
||||
await ScriptService.archiveScriptByPath({ workspace: ws, path: script.path })
|
||||
}
|
||||
}
|
||||
if (pe.drafts.has(script.path)) discardDraft(script.path)
|
||||
if (deployed) {
|
||||
pe.discardTriggerDraftsFor(script.path)
|
||||
forgetPath(script.path)
|
||||
forgetScriptCache(script.path)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== Fixes for misconfigured nodes =====================
|
||||
// Rewrites a script's annotation header and keeps the result as a draft, opened
|
||||
@@ -3523,7 +3526,11 @@
|
||||
<ConfirmationModal
|
||||
open={assetDeleteTarget != undefined}
|
||||
loading={assetDeleteLoading}
|
||||
title={assetDeleteTarget?.trigger ? 'Delete 2 items?' : 'Delete 1 item?'}
|
||||
title={assetDeleteTarget?.script && assetDeleteTarget.trigger
|
||||
? 'Delete 2 items?'
|
||||
: assetDeleteTarget?.script
|
||||
? `${canHardDeleteScripts ? 'Delete' : 'Archive'} script?`
|
||||
: 'Delete trigger?'}
|
||||
confirmationText="Delete"
|
||||
onConfirmed={confirmAssetUpstreamDelete}
|
||||
onCanceled={() => {
|
||||
@@ -3534,16 +3541,22 @@
|
||||
{@const t = assetDeleteTarget}
|
||||
<div class="flex flex-col gap-2 text-xs text-secondary">
|
||||
<p>
|
||||
{canHardDeleteScripts
|
||||
? 'The script is deleted, with every version'
|
||||
: 'The script is archived'}{t.trigger ? ', and its trigger deleted' : ''}. The tables it
|
||||
wrote stay.
|
||||
{#if t.script}
|
||||
{canHardDeleteScripts
|
||||
? 'The script is deleted, with every version'
|
||||
: 'The script is archived'}{t.trigger ? ', and its trigger deleted' : ''}. The tables
|
||||
it wrote stay.
|
||||
{:else}
|
||||
The trigger is deleted: its script stays, and no longer runs this way.
|
||||
{/if}
|
||||
</p>
|
||||
<ul class="flex flex-col gap-1">
|
||||
<li class="flex flex-col">
|
||||
<span class="text-emphasis truncate">{t.script.path}</span>
|
||||
<span class="text-2xs text-hint">Script{t.script.unsaved ? ' · draft' : ''}</span>
|
||||
</li>
|
||||
{#if t.script}
|
||||
<li class="flex flex-col">
|
||||
<span class="text-emphasis truncate">{t.script.path}</span>
|
||||
<span class="text-2xs text-hint">Script{t.script.unsaved ? ' · draft' : ''}</span>
|
||||
</li>
|
||||
{/if}
|
||||
{#if t.trigger}
|
||||
<li class="flex flex-col">
|
||||
<span class="text-emphasis truncate">{t.trigger.path}</span>
|
||||
|
||||
Reference in New Issue
Block a user