From f234df97ec3cdc480ee9d403370a3512496b024b Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:56:56 +0200 Subject: [PATCH] fix: support raw app deployment history (#8657) * fix: support raw app deployment history Co-Authored-By: Claude Opus 4.5 * fix: refresh deployment history diffs Co-Authored-By: Claude Opus 4.5 * fix: preserve deployment history preview context Co-Authored-By: Claude Opus 4.5 * fix: limit deployment history to diffs Co-Authored-By: Claude Opus 4.5 * chore: remove unused history backend hook Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- frontend/src/lib/components/DiffEditor.svelte | 56 ++++++- .../lib/components/FlowGraphDiffViewer.svelte | 37 +++-- .../apps/editor/DeploymentHistory.svelte | 153 ++++++++++++++++-- 3 files changed, 215 insertions(+), 31 deletions(-) diff --git a/frontend/src/lib/components/DiffEditor.svelte b/frontend/src/lib/components/DiffEditor.svelte index 3fb26334b9..48759109a3 100644 --- a/frontend/src/lib/components/DiffEditor.svelte +++ b/frontend/src/lib/components/DiffEditor.svelte @@ -102,7 +102,12 @@ }) } - if (defaultLang !== undefined || defaultOriginal !== undefined || defaultModified !== undefined) { + if ( + defaultLang !== undefined || + defaultOriginal !== undefined || + defaultModified !== undefined || + modifiedModel !== undefined + ) { setupModel(defaultLang ?? 'plaintext', defaultOriginal, defaultModified, defaultModifiedLang) } } @@ -113,6 +118,11 @@ modified?: string, modifiedLang?: string ) { + defaultLang = lang + defaultOriginal = original + defaultModified = modified + defaultModifiedLang = modifiedLang + const o = meditor.createModel(original ?? '', lang) const m = modifiedModel ?? meditor.createModel(modified ?? '', modifiedLang ?? lang) diffEditor?.setModel({ @@ -136,6 +146,7 @@ } export function setModifiedModel(model: meditor.ITextModel) { + modifiedModel = model const curr = diffEditor?.getModel() if (!curr) return diffEditor?.setModel({ @@ -177,6 +188,49 @@ } }) + $effect(() => { + if (!diffEditor) { + return + } + + const lang = defaultLang ?? 'plaintext' + const modifiedLang = defaultModifiedLang ?? lang + const currentModel = diffEditor.getModel() + + if (!currentModel) { + setupModel(lang, defaultOriginal, defaultModified, defaultModifiedLang) + return + } + + if (currentModel.original.getLanguageId() !== lang) { + meditor.setModelLanguage(currentModel.original, lang) + } + + const originalValue = defaultOriginal ?? '' + if (currentModel.original.getValue() !== originalValue) { + currentModel.original.setValue(originalValue) + } + + if (modifiedModel) { + if (currentModel.modified !== modifiedModel) { + diffEditor.setModel({ + original: currentModel.original, + modified: modifiedModel as meditor.ITextModel + }) + } + return + } + + if (currentModel.modified.getLanguageId() !== modifiedLang) { + meditor.setModelLanguage(currentModel.modified, modifiedLang) + } + + const modifiedValue = defaultModified ?? '' + if (currentModel.modified.getValue() !== modifiedValue) { + currentModel.modified.setValue(modifiedValue) + } + }) + onMount(() => { if (BROWSER) { return () => { diff --git a/frontend/src/lib/components/FlowGraphDiffViewer.svelte b/frontend/src/lib/components/FlowGraphDiffViewer.svelte index 62e5b208c2..3639f0955b 100644 --- a/frontend/src/lib/components/FlowGraphDiffViewer.svelte +++ b/frontend/src/lib/components/FlowGraphDiffViewer.svelte @@ -19,7 +19,6 @@ let { beforeYaml, afterYaml }: Props = $props() - let parseError = $state(undefined) let viewerWidth = $state(SIDE_BY_SIDE_MIN_WIDTH) let beforePaneSize = $state(50) let viewMode = $state<'sidebyside' | 'unified'>('sidebyside') @@ -30,25 +29,31 @@ let beforeGraph: FlowGraphV2 | undefined = $state(undefined) let afterGraph: FlowGraphV2 | undefined = $state(undefined) - let beforeFlow: OpenFlow | undefined = $derived.by(() => { + function parseFlow(yaml: string, label: 'before' | 'after'): { + flow: OpenFlow | undefined + error: string | undefined + } { try { - const parsed = YAML.parse(beforeYaml) - return parsed as OpenFlow + return { flow: YAML.parse(yaml) as OpenFlow, error: undefined } } catch (error) { - parseError = `Error parsing before flow: ${error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error'}` - return undefined + return { + flow: undefined, + error: `Error parsing ${label} flow: ${ + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Unknown error' + }` + } } - }) + } - let afterFlow: OpenFlow | undefined = $derived.by(() => { - try { - const parsed = YAML.parse(afterYaml) - return parsed as OpenFlow - } catch (error) { - parseError = `Error parsing after flow: ${error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error'}` - return undefined - } - }) + let beforeParsed = $derived.by(() => parseFlow(beforeYaml, 'before')) + let afterParsed = $derived.by(() => parseFlow(afterYaml, 'after')) + let parseError = $derived(beforeParsed.error ?? afterParsed.error) + let beforeFlow: OpenFlow | undefined = $derived(beforeParsed.flow) + let afterFlow: OpenFlow | undefined = $derived(afterParsed.flow) // Determine if we should render side-by-side or unified (user controlled via toggle) let isSideBySide = $derived(viewMode === 'sidebyside') diff --git a/frontend/src/lib/components/apps/editor/DeploymentHistory.svelte b/frontend/src/lib/components/apps/editor/DeploymentHistory.svelte index 95bf74acf8..4ae67dbb0e 100644 --- a/frontend/src/lib/components/apps/editor/DeploymentHistory.svelte +++ b/frontend/src/lib/components/apps/editor/DeploymentHistory.svelte @@ -4,29 +4,55 @@ const bubble = createBubbler() import { Pane, Splitpanes } from 'svelte-splitpanes' import PanelSection from './settingsPanel/common/PanelSection.svelte' - import { classNames, displayDate, emptyString } from '$lib/utils' + import { + classNames, + cleanValueProperties, + displayDate, + emptyString, + orderedYamlStringify, + replaceFalseWithUndefined, + type Value + } from '$lib/utils' import { AppService, type AppWithLastVersion, type AppHistory } from '$lib/gen' import { workspaceStore } from '$lib/stores' import { Skeleton } from '$lib/components/common' import Button from '$lib/components/common/button/Button.svelte' import { createEventDispatcher, untrack } from 'svelte' import { Pencil, ArrowRight, X, Loader2 } from 'lucide-svelte' + import Select from '$lib/components/select/Select.svelte' interface Props { appPath: string | undefined } + type HistoryApp = AppWithLastVersion & { value: any } + let { appPath }: Props = $props() let loading: boolean = $state(false) let versions: AppHistory[] = $state([]) let selectedVersion: AppHistory | undefined = $state(undefined) - let selected: (AppWithLastVersion & { value: any }) | undefined = $state(undefined) + let selected: HistoryApp | undefined = $state(undefined) + let previousVersion: HistoryApp | undefined = $state(undefined) + let selectedVersionIndex: number | undefined = $state(undefined) + let previousVersionId: number | undefined = $state(undefined) + let versionCache: Record = $state({}) let deploymentMsgUpdateMode = $state(false) let deploymentMsgUpdate: string | undefined = $state(undefined) + async function getVersionApp(version: number): Promise { + const cached = versionCache[version] + if (cached) { + return cached + } + + const app = await AppService.getAppByVersion({ workspace: $workspaceStore!, id: version }) + versionCache[version] = app + return app + } + async function loadVersions() { if (appPath === undefined) { return @@ -40,10 +66,18 @@ loading = false } - async function loadValue(version: number) { - let app = await AppService.getAppByVersion({ workspace: $workspaceStore!, id: version }) + async function loadSelectedVersion(version: number) { + const app = await getVersionApp(version) + if (selectedVersion?.version === version) { + selected = app + } + } - selected = app + async function loadPreviousVersion(version: number) { + const app = await getVersionApp(version) + if (previousVersionId === version) { + previousVersion = app + } } async function updateDeploymentMsg(appId: number | undefined, appVersion: number | undefined) { @@ -68,14 +102,68 @@ loadVersions() } + function toComparableVersionValue(app: HistoryApp): Value { + return app.value as Value + } + + function toVersionLabel(version: AppHistory): string { + return emptyString(version.deployment_msg) ? `Version ${version.version}` : version.deployment_msg! + } + + let availableVersions = $derived( + selectedVersionIndex !== undefined ? versions.slice(selectedVersionIndex + 1) : [] + ) + + let compareVersionItems = $derived( + availableVersions.map((version) => ({ + label: toVersionLabel(version), + value: version.version + })) + ) + + let selectedVersionYaml = $derived.by(() => { + if (!selected) return undefined + return orderedYamlStringify( + replaceFalseWithUndefined(cleanValueProperties(toComparableVersionValue(selected))) + ) + }) + + let previousVersionYaml = $derived.by(() => { + if (!previousVersion) return undefined + return orderedYamlStringify( + replaceFalseWithUndefined(cleanValueProperties(toComparableVersionValue(previousVersion))) + ) + }) + const dispatch = createEventDispatcher() loadVersions() $effect(() => { selectedVersion?.version !== undefined && untrack(() => { - selectedVersion && loadValue(selectedVersion.version!) + selectedVersion && loadSelectedVersion(selectedVersion.version!) }) }) + $effect(() => { + if (previousVersionId === undefined) { + previousVersion = undefined + return + } + + const version = previousVersionId + untrack(() => { + loadPreviousVersion(version) + }) + }) + $effect(() => { + if (availableVersions.length === 0) { + previousVersionId = undefined + return + } + + if (!availableVersions.some((version) => version.version === previousVersionId)) { + previousVersionId = availableVersions[0]?.version + } + }) @@ -85,7 +173,7 @@ {#if !loading} {#if versions.length > 0}
- {#each versions ?? [] as version} + {#each versions ?? [] as version, versionIndex}
{ selectedVersion = version + selectedVersionIndex = versionIndex + previousVersionId = versions[versionIndex + 1]?.version deploymentMsgUpdateMode = false deploymentMsgUpdate = undefined }} > - {#if emptyString(version.deployment_msg)}Version {version.version}{:else}{version.deployment_msg}{/if} + {toVersionLabel(version)}
{/each} @@ -182,7 +272,10 @@
@@ -192,11 +285,43 @@
- {#await import('$lib/components/apps/editor/AppPreview.svelte')} - - {:then Module} - - {/await} +
+ {#if availableVersions.length > 0} +
+
Compare with:
+