Compare commits

...

3 Commits

Author SHA1 Message Date
centdix
602073807f styling 2026-01-14 13:24:53 +00:00
centdix
3210157668 better 2026-01-14 13:14:41 +00:00
claude[bot]
8c67e91db4 feat(flow): add diff viewer to deployment history
Add a "Diff" tab to the flow deployment history drawer that shows the
differences between a selected historical version and the current
deployed flow. The diff view includes two sub-tabs:
- YAML: Shows the diff in YAML format using Monaco diff editor
- Graph: Shows a visual side-by-side graph comparison

Closes #7561

Co-authored-by: centdix <centdix@users.noreply.github.com>
2026-01-14 12:15:21 +00:00
5 changed files with 105 additions and 38 deletions

View File

@@ -20,7 +20,6 @@
}
let diffType: 'draft' | 'deployed' | 'custom' | undefined = $state(undefined)
let flowdiffMode: 'yaml' | 'graph' = $state('yaml')
let contentType = $derived.by(() => {
if (!data || !diffType) return undefined
@@ -232,34 +231,14 @@
{/await}
{:else if contentType === 'metadata'}
{#if isFlow}
<Tabs bind:selected={flowdiffMode}>
<Tab value="yaml" label={`YAML`} />
<Tab value="graph" label={`Graph`} />
</Tabs>
{#if flowdiffMode === 'yaml'}
{#await import('$lib/components/DiffEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
open={true}
automaticLayout
className="h-full"
defaultLang="yaml"
defaultOriginal={metadata}
defaultModified={data.current.metadata}
readOnly
/>
{/await}
{:else if flowdiffMode === 'graph'}
{#await import('$lib/components/FlowGraphDiffViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
beforeYaml={metadata ?? ''}
afterYaml={data.current.metadata}
/>
{/await}
{/if}
{#await import('$lib/components/FlowYamlGraphDiff.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
beforeYaml={metadata ?? ''}
afterYaml={data.current.metadata}
/>
{/await}
{:else}
{#await import('$lib/components/DiffEditor.svelte')}
<Loader2 class="animate-spin" />

View File

@@ -106,7 +106,6 @@
})
}
console.log('defaultModified', defaultModified)
if (defaultLang !== undefined) {
setupModel(defaultLang, defaultOriginal, defaultModified, defaultModifiedLang)
}

View File

@@ -7,24 +7,35 @@
import HighlightTheme from './HighlightTheme.svelte'
import FlowViewerInner from './FlowViewerInner.svelte'
import FlowInputViewer from './FlowInputViewer.svelte'
import { Loader2 } from 'lucide-svelte'
import {
cleanValueProperties,
orderedYamlStringify,
replaceFalseWithUndefined
} from '$lib/utils'
type FlowType = {
summary: string
description?: string
value: FlowValue
schema?: any
}
interface Props {
flow: {
summary: string
description?: string
value: FlowValue
schema?: any
}
flow: FlowType
/** Flow to compare against (for diff view) */
compareFlow?: FlowType
initialOpen?: number | undefined
noSide?: boolean
noGraph?: boolean
tab?: 'ui' | 'raw' | 'schema'
tab?: 'ui' | 'raw' | 'schema' | 'diff'
noSummary?: boolean
noGraphDownload?: boolean
}
let {
flow,
compareFlow = undefined,
initialOpen = undefined,
noSide = false,
noGraph = false,
@@ -33,6 +44,16 @@
noGraphDownload = false
}: Props = $props()
function flowToYaml(flowData: FlowType): string {
const cleaned = structuredClone(
cleanValueProperties(replaceFalseWithUndefined(flowData))
)
return orderedYamlStringify(cleaned)
}
const beforeYaml = $derived(compareFlow ? flowToYaml(compareFlow) : '')
const afterYaml = $derived(flowToYaml(flow))
let open: { [id: number]: boolean } = {}
if (initialOpen) {
open[initialOpen] = true
@@ -47,6 +68,9 @@
{/if}
<Tab value="raw" label="Raw" />
<Tab value="schema" label="Input Schema" />
{#if compareFlow}
<Tab value="diff" label="Diff" />
{/if}
{#snippet content()}
<TabContent value="ui">
@@ -75,5 +99,16 @@
<div class="my-4"></div>
<SchemaViewer schema={flow.schema} />
</TabContent>
{#if compareFlow}
<TabContent value="diff">
<div class="h-[calc(100vh-300px)] min-h-[400px] mt-4">
{#await import('$lib/components/FlowYamlGraphDiff.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default {beforeYaml} {afterYaml} />
{/await}
</div>
</TabContent>
{/if}
{/snippet}
</Tabs>

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import { Loader2 } from 'lucide-svelte'
import Tabs from './common/tabs/Tabs.svelte'
import Tab from './common/tabs/Tab.svelte'
interface Props {
beforeYaml: string
afterYaml: string
}
let { beforeYaml, afterYaml }: Props = $props()
let diffMode: 'yaml' | 'graph' = $state('yaml')
</script>
<Tabs bind:selected={diffMode}>
<Tab value="yaml" label="YAML" />
<Tab value="graph" label="Graph" />
</Tabs>
{#if diffMode === 'yaml'}
{#await import('$lib/components/DiffEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
open={true}
automaticLayout
className="h-full"
defaultLang="yaml"
defaultOriginal={beforeYaml}
defaultModified={afterYaml}
readOnly
/>
{/await}
{:else if diffMode === 'graph'}
{#await import('$lib/components/FlowGraphDiffViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default {beforeYaml} {afterYaml} />
{/await}
{/if}

View File

@@ -23,6 +23,7 @@
let selectedVersion: FlowVersion | undefined = $state(undefined)
let selected: Flow | undefined = $state(undefined)
let currentFlow: Flow | undefined = $state(undefined)
let deploymentMsgUpdateMode = $state(false)
let deploymentMsgUpdate: string | undefined = $state(undefined)
@@ -33,6 +34,18 @@
})
}
async function loadCurrentFlow() {
try {
currentFlow = await FlowService.getFlowByPath({
workspace: $workspaceStore!,
path
})
} catch {
// Current flow might not exist (e.g., deleted), ignore
currentFlow = undefined
}
}
async function loadVersions() {
loading = true
versions = await FlowService.getFlowHistory({
@@ -77,6 +90,7 @@
}
loadVersions()
loadCurrentFlow()
run(() => {
selectedVersion !== undefined && loadFlow(selectedVersion.id)
@@ -208,7 +222,7 @@
{#await import('$lib/components/FlowViewer.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default flow={selected} />
<Module.default flow={selected} compareFlow={currentFlow} />
{/await}
</div>
{:else}