mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
3cfd858e36
Augments the backend fork-vs-parent comparison with browser-local (UserDraft) drafts so a session's uncommitted AI/user changes are visible in the Fork Diff Viewer and the /forks/compare page. Adds forkDraftDiff.ts (augmentForkComparisonWithLocalDrafts + getForkItemValue), a 'local changes detected' / new-draft warning surface (checkbox-slot warning icon, no-op-baseline filtering, dedup), a 'Local draft <> fork' tab in DiffDrawer, and selectTooltip/nonSelectableTooltip plumbing in Row/WorkspaceDeployLayout. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
166 lines
4.3 KiB
Svelte
166 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import { Loader2 } from 'lucide-svelte'
|
|
import { Badge } from './common'
|
|
import Row from './common/table/Row.svelte'
|
|
import Tooltip from './Tooltip.svelte'
|
|
import type { Snippet } from 'svelte'
|
|
import type { Kind } from '$lib/utils_deployable'
|
|
|
|
interface DeployableItem {
|
|
key: string
|
|
path: string
|
|
kind: Kind
|
|
triggerKind?: string
|
|
[key: string]: unknown
|
|
}
|
|
|
|
interface Props {
|
|
items: DeployableItem[]
|
|
selectedItems: string[]
|
|
selectablePredicate?: (item: DeployableItem) => boolean
|
|
/** For non-selectable items, an optional warning shown in the checkbox
|
|
* slot explaining why it can't be selected. When provided the row keeps
|
|
* full opacity (the warning icon carries the meaning); other
|
|
* non-selectable rows dim as before. */
|
|
nonSelectableTooltip?: (item: DeployableItem) => string | undefined
|
|
deploymentStatus: Record<string, { status: 'loading' | 'deployed' | 'failed'; error?: string }>
|
|
allSelected?: boolean
|
|
emptyMessage?: string
|
|
children?: Snippet
|
|
|
|
// Snippets for customization
|
|
header?: Snippet
|
|
alerts?: Snippet
|
|
itemSummary?: Snippet<[DeployableItem]>
|
|
itemActions?: Snippet<[DeployableItem]>
|
|
footer?: Snippet
|
|
|
|
// Callbacks
|
|
onToggleItem?: (item: DeployableItem) => void
|
|
onSelectAll?: () => void
|
|
onDeselectAll?: () => void
|
|
}
|
|
|
|
let {
|
|
items,
|
|
selectedItems,
|
|
selectablePredicate = () => true,
|
|
nonSelectableTooltip = () => undefined,
|
|
deploymentStatus,
|
|
allSelected = false,
|
|
emptyMessage = 'No items to deploy',
|
|
header,
|
|
alerts,
|
|
itemSummary,
|
|
itemActions,
|
|
footer,
|
|
onToggleItem,
|
|
onSelectAll,
|
|
onDeselectAll
|
|
}: Props = $props()
|
|
|
|
let selectableItems = $derived(items.filter(selectablePredicate))
|
|
let hasSelectableItems = $derived(selectableItems.length > 0)
|
|
</script>
|
|
|
|
<div class="flex flex-col h-full">
|
|
<!-- Header section -->
|
|
{#if header}
|
|
<div class="bg-surface">
|
|
{@render header()}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Alerts section -->
|
|
{#if alerts}
|
|
{@render alerts()}
|
|
{/if}
|
|
|
|
{#if items.length > 0}
|
|
<!-- Select all row -->
|
|
<div class="px-4 py-2 flex items-center justify-between">
|
|
<div
|
|
class="flex items-center gap-2 text-secondary text-xs"
|
|
class:opacity-50={!hasSelectableItems}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
disabled={!hasSelectableItems}
|
|
checked={allSelected}
|
|
onchange={allSelected ? onDeselectAll : onSelectAll}
|
|
class="rounded max-w-4 w-full"
|
|
/> Select all
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Items list -->
|
|
<div class="overflow-y-auto">
|
|
<div class="border rounded-md bg-surface-tertiary">
|
|
{#each items as item (item.key)}
|
|
{@const isSelectable = selectablePredicate(item)}
|
|
{@const isSelected = selectedItems.includes(item.key)}
|
|
{@const status = deploymentStatus[item.key]}
|
|
{@const isDeployed = status?.status === 'deployed'}
|
|
{@const softReason = !isSelectable ? nonSelectableTooltip(item) : undefined}
|
|
|
|
<Row
|
|
isSelectable={isSelectable && !isDeployed}
|
|
alignWithSelectable={true}
|
|
selectTooltip={softReason}
|
|
disabled={!isSelectable && !softReason}
|
|
selected={isSelected && !isDeployed}
|
|
onSelect={() => onToggleItem?.(item)}
|
|
path={item.kind !== 'resource' &&
|
|
item.kind !== 'variable' &&
|
|
item.kind !== 'resource_type'
|
|
? item.path
|
|
: ''}
|
|
marked={undefined}
|
|
kind={item.kind}
|
|
triggerKind={item.triggerKind}
|
|
canFavorite={false}
|
|
workspaceId=""
|
|
>
|
|
{#snippet customSummary()}
|
|
{#if itemSummary}
|
|
{@render itemSummary(item)}
|
|
{:else}
|
|
{item.path}
|
|
{/if}
|
|
{/snippet}
|
|
{#snippet actions()}
|
|
{#if itemActions}
|
|
{@render itemActions(item)}
|
|
{/if}
|
|
<!-- Deployment status always shown -->
|
|
{#if status}
|
|
{#if status.status === 'loading'}
|
|
<Loader2 class="animate-spin" />
|
|
{:else if status.status === 'deployed'}
|
|
<Badge color="green">Deployed</Badge>
|
|
{:else if status.status === 'failed'}
|
|
<div class="inline-flex gap-1">
|
|
<Badge color="red">Failed</Badge>
|
|
<Tooltip>{status.error}</Tooltip>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
{/snippet}
|
|
</Row>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="flex items-center justify-center h-full">
|
|
<div class="text-hint text-xs">{emptyMessage}</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Footer -->
|
|
{#if footer}
|
|
<div class="pt-4">
|
|
{@render footer()}
|
|
</div>
|
|
{/if}
|
|
</div>
|