fix: support raw app deployment history (#8657)
* fix: support raw app deployment history Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: refresh deployment history diffs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: preserve deployment history preview context Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: limit deployment history to diffs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: remove unused history backend hook Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
let { beforeYaml, afterYaml }: Props = $props()
|
||||
|
||||
let parseError = $state<string | undefined>(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')
|
||||
|
||||
@@ -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<number, HistoryApp> = $state({})
|
||||
|
||||
let deploymentMsgUpdateMode = $state(false)
|
||||
let deploymentMsgUpdate: string | undefined = $state(undefined)
|
||||
|
||||
async function getVersionApp(version: number): Promise<HistoryApp> {
|
||||
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
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Splitpanes class="!overflow-visible">
|
||||
@@ -85,7 +173,7 @@
|
||||
{#if !loading}
|
||||
{#if versions.length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each versions ?? [] as version}
|
||||
{#each versions ?? [] as version, versionIndex}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
@@ -95,12 +183,14 @@
|
||||
)}
|
||||
onclick={() => {
|
||||
selectedVersion = version
|
||||
selectedVersionIndex = versionIndex
|
||||
previousVersionId = versions[versionIndex + 1]?.version
|
||||
deploymentMsgUpdateMode = false
|
||||
deploymentMsgUpdate = undefined
|
||||
}}
|
||||
>
|
||||
<span class="text-xs truncate">
|
||||
{#if emptyString(version.deployment_msg)}Version {version.version}{:else}{version.deployment_msg}{/if}
|
||||
{toVersionLabel(version)}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -182,7 +272,10 @@
|
||||
<div class="flex p-1 gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() => window.open(`/apps/add?template_id=${selectedVersion?.version}`)}
|
||||
on:click={() =>
|
||||
window.open(
|
||||
`${selected?.raw_app ? '/apps_raw' : '/apps'}/add?template_id=${selectedVersion?.version}`
|
||||
)}
|
||||
>
|
||||
Restore as fork
|
||||
</Button>
|
||||
@@ -192,11 +285,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#await import('$lib/components/apps/editor/AppPreview.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default noBackend app={selected.value} context={{}} />
|
||||
{/await}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if availableVersions.length > 0}
|
||||
<div class="flex flex-row items-center gap-2 py-2">
|
||||
<div class="text-xs">Compare with:</div>
|
||||
<Select
|
||||
items={compareVersionItems}
|
||||
bind:value={previousVersionId}
|
||||
class="w-56"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-[calc(100vh-260px)] min-h-[420px]">
|
||||
{#if previousVersionYaml && selectedVersionYaml}
|
||||
{#await import('$lib/components/DiffEditor.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
open={true}
|
||||
automaticLayout
|
||||
className="h-full"
|
||||
defaultLang="yaml"
|
||||
defaultOriginal={previousVersionYaml}
|
||||
defaultModified={selectedVersionYaml}
|
||||
readOnly
|
||||
/>
|
||||
{/await}
|
||||
{:else}
|
||||
<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm p-2 text-primary">
|
||||
No older deployment is available to compare with this version.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Skeleton layout={[[40]]} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user