feat(nacos): improve config history and diff viewing
This commit is contained in:
18 files changed
+508
-167
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, shallowRef, useId, watch } from "vue";
|
||||
import { Compartment, StateEffect, StateField, type Extension } from "@codemirror/state";
|
||||
import { StreamLanguage, ensureSyntaxTree } from "@codemirror/language";
|
||||
import { Compartment, StateEffect, StateField } from "@codemirror/state";
|
||||
import { ensureSyntaxTree } from "@codemirror/language";
|
||||
import { Decoration, EditorView } from "@codemirror/view";
|
||||
import { Archive, ArrowLeftRight, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Clipboard, Columns3, Download, ExternalLink, FileClock, FileInput, FileText, Loader2, Maximize2, Minimize2, Network, Plus, RefreshCw, Save, Search, Send, Server, Trash2, X } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -59,6 +59,7 @@ import { executeWithProductionContextGuard } from "@/lib/database/productionExec
|
||||
import { productionContextForDatabase } from "@/lib/database/productionSafety";
|
||||
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
|
||||
import { validateNacosConfigContent, type NacosConfigDiagnostic } from "@/lib/nacos/nacosConfigValidation";
|
||||
import { loadNacosConfigLanguage, resolveNacosConfigFormat } from "@/lib/nacos/nacosConfigLanguage";
|
||||
import type { NacosConfigEditorViewport } from "@/types/database";
|
||||
import type {
|
||||
NacosBatchPreview,
|
||||
@@ -158,10 +159,12 @@ const historyPageSize = ref(20);
|
||||
const historyTotal = ref(0);
|
||||
const historyViewingItem = ref<NacosConfigHistoryItem | null>(null);
|
||||
const historyViewingContent = ref("");
|
||||
const historyViewingFormat = ref("text");
|
||||
const historyViewingLoading = ref(false);
|
||||
const historyCompareOpen = ref(false);
|
||||
const historyCompareCurrent = ref("");
|
||||
const historyCompareContent = ref("");
|
||||
const historyCompareFormat = ref("text");
|
||||
const historyCompareLoading = ref(false);
|
||||
const historyCompareItem = ref<NacosConfigHistoryItem | null>(null);
|
||||
const pendingHistoryRollback = ref<NacosConfigHistoryItem | null>(null);
|
||||
@@ -501,37 +504,6 @@ function currentCustomThemeColors() {
|
||||
return activeTheme?.colors ?? settings.customThemeColors;
|
||||
}
|
||||
|
||||
async function configLanguageExtension(format: string): Promise<Extension[]> {
|
||||
switch (format) {
|
||||
case "json": {
|
||||
const { json } = await import("@codemirror/lang-json");
|
||||
return [json()];
|
||||
}
|
||||
case "yaml": {
|
||||
const { yaml } = await import("@codemirror/lang-yaml");
|
||||
return [yaml()];
|
||||
}
|
||||
case "xml": {
|
||||
const { xml } = await import("@codemirror/lang-xml");
|
||||
return [xml()];
|
||||
}
|
||||
case "html": {
|
||||
const { html } = await import("@codemirror/lang-html");
|
||||
return [html({ matchClosingTags: false })];
|
||||
}
|
||||
case "properties": {
|
||||
const { properties } = await import("@codemirror/legacy-modes/mode/properties");
|
||||
return [StreamLanguage.define(properties)];
|
||||
}
|
||||
case "toml": {
|
||||
const { toml } = await import("@codemirror/legacy-modes/mode/toml");
|
||||
return [StreamLanguage.define(toml)];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function configValidationHighlightExtension() {
|
||||
const decorationsFor = (state: import("@codemirror/state").EditorState, diagnostics: NacosConfigDiagnostic[]) => {
|
||||
const ranges = diagnostics
|
||||
@@ -577,7 +549,7 @@ async function mountConfigEditor() {
|
||||
import("codemirror"),
|
||||
import("@codemirror/commands"),
|
||||
import("@codemirror/search"),
|
||||
configLanguageExtension(format),
|
||||
loadNacosConfigLanguage(format),
|
||||
]);
|
||||
const editorSettings = settingsStore.editorSettings;
|
||||
configEditorFontSize.value = clampEditorFontSize(editorSettings.fontSize);
|
||||
@@ -1680,9 +1652,11 @@ async function viewConfigHistory(item: NacosConfigHistoryItem) {
|
||||
await nextTick();
|
||||
historyViewingItem.value = item;
|
||||
historyViewingContent.value = "";
|
||||
historyViewingFormat.value = resolveNacosConfigFormat(item.configType, item.dataId);
|
||||
historyViewingLoading.value = true;
|
||||
const detail = await loadHistoryDetail(item);
|
||||
historyViewingContent.value = detail?.content || "";
|
||||
historyViewingFormat.value = resolveNacosConfigFormat(detail?.configType || item.configType, detail?.dataId || item.dataId);
|
||||
historyViewingLoading.value = false;
|
||||
}
|
||||
|
||||
@@ -1690,6 +1664,7 @@ function closeHistoryDetail() {
|
||||
historyViewingItem.value = null;
|
||||
historyViewingContent.value = "";
|
||||
historyViewingLoading.value = false;
|
||||
historyViewingFormat.value = "text";
|
||||
}
|
||||
|
||||
async function compareConfigHistory(item: NacosConfigHistoryItem) {
|
||||
@@ -1699,10 +1674,12 @@ async function compareConfigHistory(item: NacosConfigHistoryItem) {
|
||||
historyCompareItem.value = item;
|
||||
historyCompareCurrent.value = "";
|
||||
historyCompareContent.value = "";
|
||||
historyCompareFormat.value = resolveNacosConfigFormat(item.configType, item.dataId);
|
||||
try {
|
||||
const [current, history] = await Promise.all([api.nacosGetConfig(props.connectionId, selectedConfigOriginalKey.value), api.nacosGetConfigHistory(props.connectionId, historyKeyFor(item))]);
|
||||
historyCompareCurrent.value = current.content || "";
|
||||
historyCompareContent.value = history.content || "";
|
||||
historyCompareFormat.value = resolveNacosConfigFormat(history.configType || item.configType || current.configType, history.dataId || item.dataId);
|
||||
} catch (error) {
|
||||
await handleRNacosConsoleError(error, () => compareConfigHistory(item), "history");
|
||||
historyCompareOpen.value = false;
|
||||
@@ -3341,7 +3318,7 @@ function openNacosConsole() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<NacosConfigDiffDialog v-model:open="pendingConfigSave" :before="originalConfigContent" :after="configContent" :loading="savingConfig" @confirm="saveConfig" />
|
||||
<NacosConfigDiffDialog v-model:open="pendingConfigSave" :before="originalConfigContent" :after="configContent" :format="resolveNacosConfigFormat(configType, configDataId)" :loading="savingConfig" @confirm="saveConfig" />
|
||||
|
||||
<NacosContentSearchDialog
|
||||
v-model:open="searchOpen"
|
||||
@@ -3394,6 +3371,7 @@ function openNacosConsole() {
|
||||
:read-only="readOnly"
|
||||
:viewing-item="historyViewingItem"
|
||||
:viewing-content="historyViewingContent"
|
||||
:viewing-format="historyViewingFormat"
|
||||
:viewing-loading="historyViewingLoading"
|
||||
@load="loadConfigHistory"
|
||||
@view="viewConfigHistory"
|
||||
@@ -3433,6 +3411,7 @@ function openNacosConsole() {
|
||||
:after-label="t('nacos.historyVersionContent')"
|
||||
:before="historyCompareCurrent"
|
||||
:after="historyCompareContent"
|
||||
:format="historyCompareFormat"
|
||||
:loading="historyCompareLoading"
|
||||
:show-confirm="!readOnly"
|
||||
:confirm-label="t('nacos.rollback')"
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView, lineNumbers } from "@codemirror/view";
|
||||
import { basicSetup } from "codemirror";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { editorFontTheme, loadEditorTheme } from "@/lib/editor/editorThemes";
|
||||
import { loadNacosConfigLanguage, resolveNacosConfigFormat } from "@/lib/nacos/nacosConfigLanguage";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
content: string;
|
||||
format?: string;
|
||||
dataId?: string;
|
||||
lineNumbers?: boolean;
|
||||
}>(),
|
||||
{ format: "", dataId: "", lineNumbers: true },
|
||||
);
|
||||
|
||||
const host = ref<HTMLElement | null>(null);
|
||||
const settingsStore = useSettingsStore();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
let view: EditorView | null = null;
|
||||
let generation = 0;
|
||||
|
||||
function currentCustomThemeColors() {
|
||||
const settings = settingsStore.editorSettings;
|
||||
if (settings.theme !== "custom") return settings.customThemeColors;
|
||||
const activeTheme = settings.customThemes?.find((theme) => theme.id === settings.activeCustomThemeId) || settings.customThemes?.[0];
|
||||
return activeTheme?.colors ?? settings.customThemeColors;
|
||||
}
|
||||
|
||||
async function mount() {
|
||||
const target = host.value;
|
||||
if (!target) return;
|
||||
const currentGeneration = ++generation;
|
||||
view?.destroy();
|
||||
view = null;
|
||||
const format = resolveNacosConfigFormat(props.format, props.dataId);
|
||||
const [language, theme] = await Promise.all([loadNacosConfigLanguage(format), loadEditorTheme(settingsStore.editorSettings.theme, isDark.value ? "dark" : "light", currentCustomThemeColors(), themePalette.value)]);
|
||||
if (currentGeneration !== generation || target !== host.value) return;
|
||||
const state = EditorState.create({
|
||||
doc: props.content,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
language,
|
||||
theme,
|
||||
editorFontTheme(EditorView, settingsStore.editorSettings.fontSize, settingsStore.editorSettings.fontFamily, { fixedHeight: true, scrollable: true }),
|
||||
...(props.lineNumbers ? [lineNumbers()] : []),
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.editable.of(false),
|
||||
EditorView.theme({
|
||||
"&": { height: "100%", backgroundColor: "transparent" },
|
||||
".cm-scroller": { overflow: "auto" },
|
||||
".cm-content": { minHeight: "100%", userSelect: "text", WebkitUserSelect: "text" },
|
||||
".cm-lineNumbers .cm-gutterElement": { padding: "0 10px 0 8px" },
|
||||
}),
|
||||
],
|
||||
});
|
||||
view = new EditorView({ parent: target, state });
|
||||
}
|
||||
|
||||
onMounted(() => void mount());
|
||||
watch(
|
||||
() => [props.content, props.format, props.dataId, props.lineNumbers],
|
||||
() => void mount(),
|
||||
);
|
||||
onBeforeUnmount(() => {
|
||||
generation += 1;
|
||||
view?.destroy();
|
||||
view = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="host" class="nacos-config-code-viewer h-full min-h-0 overflow-hidden rounded-md border bg-background" />
|
||||
</template>
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { Loader2 } from "@lucide/vue";
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { ChevronDown, ChevronUp, Loader2, X } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { buildNacosInlineDiff, buildNacosSideBySideDiff, summarizeNacosConfigDiff, type NacosDiffLineType, type NacosInlineSegment } from "@/lib/nacos/nacosAdmin";
|
||||
import NacosConfigDiffPane from "@/components/nacos/NacosConfigDiffPane.vue";
|
||||
import { buildNacosInlineDiff, buildNacosSideBySideDiff, type NacosInlineDiffRow } from "@/lib/nacos/nacosAdmin";
|
||||
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
|
||||
@@ -19,6 +20,7 @@ const props = withDefaults(
|
||||
confirmVariant?: "default" | "destructive";
|
||||
showConfirm?: boolean;
|
||||
loading?: boolean;
|
||||
format?: string;
|
||||
}>(),
|
||||
{
|
||||
title: "",
|
||||
@@ -28,6 +30,7 @@ const props = withDefaults(
|
||||
confirmVariant: "default",
|
||||
showConfirm: true,
|
||||
loading: false,
|
||||
format: "text",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -39,7 +42,23 @@ const { t } = useI18n();
|
||||
const inlineCompare = ref(false);
|
||||
const rows = computed(() => buildNacosSideBySideDiff(props.before, props.after));
|
||||
const inlineRows = computed(() => buildNacosInlineDiff(props.before, props.after));
|
||||
const summary = computed(() => summarizeNacosConfigDiff(props.before, props.after));
|
||||
const changedRows = computed(() => rows.value.map((row, index) => ({ row, index })).filter(({ row }) => row.leftType !== "equal" || row.rightType !== "equal"));
|
||||
const diffStats = computed(() => {
|
||||
let added = 0;
|
||||
let removed = 0;
|
||||
let modified = 0;
|
||||
for (const { row } of changedRows.value) {
|
||||
if (row.leftType === "modify" && row.rightType === "modify") modified += 1;
|
||||
else if (row.leftType === "padding" && row.rightType === "insert") added += 1;
|
||||
else if (row.leftType === "delete" && row.rightType === "padding") removed += 1;
|
||||
}
|
||||
return { added, removed, modified };
|
||||
});
|
||||
const activeChange = ref(0);
|
||||
const activeRowIndex = computed(() => changedRows.value[activeChange.value]?.index ?? null);
|
||||
const leftPane = ref<InstanceType<typeof NacosConfigDiffPane> | null>(null);
|
||||
const rightPane = ref<InstanceType<typeof NacosConfigDiffPane> | null>(null);
|
||||
let detachScrollSync: (() => void) | null = null;
|
||||
|
||||
const dialogOpen = computed({
|
||||
get: () => open.value,
|
||||
@@ -49,69 +68,29 @@ const dialogOpen = computed({
|
||||
},
|
||||
});
|
||||
|
||||
function lineClass(type: NacosDiffLineType, side: "left" | "right") {
|
||||
function inlineRowClass(type: NacosInlineDiffRow["type"]) {
|
||||
return {
|
||||
"bg-red-500/20 text-red-50": type === "delete" || (type === "modify" && side === "left"),
|
||||
"bg-emerald-500/18 text-emerald-50": type === "insert" || (type === "modify" && side === "right"),
|
||||
"text-zinc-200": type === "equal",
|
||||
"bg-zinc-900/80 text-zinc-600": type === "padding",
|
||||
"bg-red-500/20 text-red-700 dark:text-red-50": type === "delete",
|
||||
"bg-emerald-500/18 text-emerald-700 dark:text-emerald-50": type === "insert",
|
||||
"text-foreground": type === "equal",
|
||||
};
|
||||
}
|
||||
|
||||
function gutterClass(type: NacosDiffLineType, side: "left" | "right") {
|
||||
function inlineGutterClass(type: NacosInlineDiffRow["type"]) {
|
||||
return {
|
||||
"text-red-300": type === "delete" || (type === "modify" && side === "left"),
|
||||
"text-emerald-300": type === "insert" || (type === "modify" && side === "right"),
|
||||
"text-zinc-500": type === "equal" || type === "padding",
|
||||
"text-red-600 dark:text-red-300": type === "delete",
|
||||
"text-emerald-600 dark:text-emerald-300": type === "insert",
|
||||
"text-muted-foreground": type === "equal",
|
||||
};
|
||||
}
|
||||
|
||||
function prefix(type: NacosDiffLineType, side: "left" | "right") {
|
||||
if (type === "delete" || (type === "modify" && side === "left")) return "-";
|
||||
if (type === "insert" || (type === "modify" && side === "right")) return "+";
|
||||
return "";
|
||||
}
|
||||
|
||||
function inlineSegments(content: string, segments: NacosInlineSegment[]) {
|
||||
return segments.length ? segments : [{ value: content, changed: false }];
|
||||
}
|
||||
|
||||
function inlineClass(type: NacosDiffLineType, changed: boolean) {
|
||||
if (!changed) return "";
|
||||
if (type === "delete" || type === "modify") return "nacos-inline-change rounded-[2px] bg-red-500/80 text-red-50";
|
||||
if (type === "insert") return "nacos-inline-change rounded-[2px] bg-emerald-500/75 text-emerald-50";
|
||||
return "";
|
||||
}
|
||||
|
||||
function rightInlineClass(type: NacosDiffLineType, changed: boolean) {
|
||||
if (!changed) return "";
|
||||
if (type === "modify" || type === "insert") return "nacos-inline-change rounded-[2px] bg-emerald-500/75 text-emerald-50";
|
||||
return inlineClass(type, changed);
|
||||
}
|
||||
|
||||
function inlineRowClass(type: "equal" | "delete" | "insert") {
|
||||
return {
|
||||
"bg-red-500/20 text-red-50": type === "delete",
|
||||
"bg-emerald-500/18 text-emerald-50": type === "insert",
|
||||
"text-zinc-200": type === "equal",
|
||||
};
|
||||
}
|
||||
|
||||
function inlineGutterClass(type: "equal" | "delete" | "insert") {
|
||||
return {
|
||||
"text-red-300": type === "delete",
|
||||
"text-emerald-300": type === "insert",
|
||||
"text-zinc-500": type === "equal",
|
||||
};
|
||||
}
|
||||
|
||||
function inlinePrefix(type: "equal" | "delete" | "insert") {
|
||||
function inlinePrefix(type: NacosInlineDiffRow["type"]) {
|
||||
if (type === "delete") return "-";
|
||||
if (type === "insert") return "+";
|
||||
return "";
|
||||
}
|
||||
|
||||
function inlineRowSegmentClass(type: "equal" | "delete" | "insert", changed: boolean) {
|
||||
function inlineRowSegmentClass(type: NacosInlineDiffRow["type"], changed: boolean) {
|
||||
if (!changed) return "";
|
||||
if (type === "delete") return "nacos-inline-change rounded-[2px] bg-red-500/80 text-red-50";
|
||||
if (type === "insert") return "nacos-inline-change rounded-[2px] bg-emerald-500/75 text-emerald-50";
|
||||
@@ -122,6 +101,78 @@ function onConfirm() {
|
||||
if (props.loading) return;
|
||||
emit("confirm");
|
||||
}
|
||||
|
||||
function bindScrollSync() {
|
||||
detachScrollSync?.();
|
||||
detachScrollSync = null;
|
||||
const left = leftPane.value?.scrollerElement();
|
||||
const right = rightPane.value?.scrollerElement();
|
||||
if (!left || !right) return;
|
||||
let syncing = false;
|
||||
const sync = (source: HTMLElement, target: HTMLElement) => {
|
||||
if (syncing) return;
|
||||
syncing = true;
|
||||
target.scrollTop = source.scrollTop;
|
||||
target.scrollLeft = source.scrollLeft;
|
||||
requestAnimationFrame(() => {
|
||||
syncing = false;
|
||||
});
|
||||
};
|
||||
const onLeft = () => sync(left, right);
|
||||
const onRight = () => sync(right, left);
|
||||
left.addEventListener("scroll", onLeft, { passive: true });
|
||||
right.addEventListener("scroll", onRight, { passive: true });
|
||||
detachScrollSync = () => {
|
||||
left.removeEventListener("scroll", onLeft);
|
||||
right.removeEventListener("scroll", onRight);
|
||||
};
|
||||
}
|
||||
|
||||
function onPaneReady() {
|
||||
if (!open.value || inlineCompare.value) return;
|
||||
bindScrollSync();
|
||||
const rowIndex = activeRowIndex.value;
|
||||
if (rowIndex != null) {
|
||||
leftPane.value?.scrollToRow(rowIndex);
|
||||
rightPane.value?.scrollToRow(rowIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateChange(direction: 1 | -1) {
|
||||
if (!changedRows.value.length) return;
|
||||
activeChange.value = (activeChange.value + direction + changedRows.value.length) % changedRows.value.length;
|
||||
const rowIndex = changedRows.value[activeChange.value]?.index;
|
||||
if (rowIndex == null) return;
|
||||
leftPane.value?.scrollToRow(rowIndex);
|
||||
rightPane.value?.scrollToRow(rowIndex);
|
||||
}
|
||||
|
||||
watch(
|
||||
[open, rows],
|
||||
async () => {
|
||||
activeChange.value = 0;
|
||||
if (!open.value) {
|
||||
detachScrollSync?.();
|
||||
detachScrollSync = null;
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
bindScrollSync();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(inlineCompare, async (value) => {
|
||||
if (value) {
|
||||
detachScrollSync?.();
|
||||
detachScrollSync = null;
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
bindScrollSync();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => detachScrollSync?.());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -130,47 +181,42 @@ function onConfirm() {
|
||||
<DialogHeader class="shrink-0 border-b px-5 py-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<DialogTitle class="text-lg font-semibold">{{ title || t("nacos.configDiffTitle") }}</DialogTitle>
|
||||
<button type="button" class="text-2xl leading-none text-muted-foreground hover:text-foreground" :disabled="loading" @click="open = false">×</button>
|
||||
<Button size="icon" variant="ghost" class="h-8 w-8 shrink-0" :disabled="loading" :aria-label="t('dangerDialog.cancel')" @click="open = false"><X class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap items-center justify-between gap-3">
|
||||
<label class="inline-flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input v-model="inlineCompare" type="checkbox" class="h-4 w-4 rounded border-border" />
|
||||
<span>{{ t("nacos.inlineCompare") }}</span>
|
||||
</label>
|
||||
<div class="text-xs text-muted-foreground">{{ t("nacos.confirmSaveMessage", { added: summary.addedLines, removed: summary.removedLines }) }}</div>
|
||||
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span>{{ t("nacos.diffStats", diffStats) }}</span>
|
||||
<div v-if="changedRows.length && !inlineCompare" class="inline-flex items-center gap-1">
|
||||
<Button size="icon" variant="ghost" class="h-6 w-6" :title="t('nacos.previousDifference')" :aria-label="t('nacos.previousDifference')" @click="navigateChange(-1)"><ChevronUp class="h-3.5 w-3.5" /></Button>
|
||||
<span>{{ activeChange + 1 }} / {{ changedRows.length }}</span>
|
||||
<Button size="icon" variant="ghost" class="h-6 w-6" :title="t('nacos.nextDifference')" :aria-label="t('nacos.nextDifference')" @click="navigateChange(1)"><ChevronDown class="h-3.5 w-3.5" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="!inlineCompare" class="grid min-h-0 flex-1 grid-cols-1 gap-4 bg-background px-5 py-4 lg:grid-cols-2">
|
||||
<section class="flex min-w-0 min-h-0 flex-col">
|
||||
<div class="mb-2 text-sm font-medium text-foreground">{{ beforeLabel || t("nacos.currentVersionContent") }}</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto rounded-sm border border-zinc-700 bg-[#1f1f1f] font-mono text-[13px] leading-6 text-zinc-200">
|
||||
<div v-for="row in rows" :key="`${row.id}-left`" class="grid min-w-max grid-cols-[52px_22px_minmax(48rem,1fr)]" :class="lineClass(row.leftType, 'left')">
|
||||
<span class="select-none border-r border-white/8 pr-2 text-right" :class="gutterClass(row.leftType, 'left')">{{ row.leftLineNumber ?? "" }}</span>
|
||||
<span class="select-none pl-2" :class="gutterClass(row.leftType, 'left')">{{ prefix(row.leftType, "left") }}</span>
|
||||
<pre class="whitespace-pre px-2"><template v-for="(segment, index) in inlineSegments(row.leftContent, row.leftInline)" :key="index"><span :class="inlineClass(row.leftType, segment.changed)">{{ segment.value }}</span></template></pre>
|
||||
</div>
|
||||
</div>
|
||||
<NacosConfigDiffPane ref="leftPane" :rows="rows" side="left" :format="format" :active-row="activeRowIndex" class="min-h-0 flex-1" @ready="onPaneReady" />
|
||||
</section>
|
||||
|
||||
<section class="flex min-w-0 min-h-0 flex-col">
|
||||
<div class="mb-2 text-sm font-medium text-foreground">{{ afterLabel || t("nacos.publishVersionContent") }}</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto rounded-sm border border-zinc-700 bg-[#1f1f1f] font-mono text-[13px] leading-6 text-zinc-200">
|
||||
<div v-for="row in rows" :key="`${row.id}-right`" class="grid min-w-max grid-cols-[52px_22px_minmax(48rem,1fr)]" :class="lineClass(row.rightType, 'right')">
|
||||
<span class="select-none border-r border-white/8 pr-2 text-right" :class="gutterClass(row.rightType, 'right')">{{ row.rightLineNumber ?? "" }}</span>
|
||||
<span class="select-none pl-2" :class="gutterClass(row.rightType, 'right')">{{ prefix(row.rightType, "right") }}</span>
|
||||
<pre class="whitespace-pre px-2"><template v-for="(segment, index) in inlineSegments(row.rightContent, row.rightInline)" :key="index"><span :class="rightInlineClass(row.rightType, segment.changed)">{{ segment.value }}</span></template></pre>
|
||||
</div>
|
||||
</div>
|
||||
<NacosConfigDiffPane ref="rightPane" :rows="rows" side="right" :format="format" :active-row="activeRowIndex" class="min-h-0 flex-1" @ready="onPaneReady" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div v-else class="min-h-0 flex-1 bg-background px-5 py-4">
|
||||
<section class="flex h-full min-h-0 flex-col">
|
||||
<div class="mb-2 text-sm font-medium text-foreground">{{ t("nacos.inlineCompare") }}</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto rounded-sm border border-zinc-700 bg-[#1f1f1f] font-mono text-[13px] leading-6 text-zinc-200">
|
||||
<div class="min-h-0 flex-1 overflow-auto rounded-sm border bg-background font-mono text-[13px] leading-6 text-foreground">
|
||||
<div v-for="row in inlineRows" :key="row.id" class="grid min-w-max grid-cols-[52px_22px_minmax(96rem,1fr)]" :class="inlineRowClass(row.type)">
|
||||
<span class="select-none border-r border-white/8 pr-2 text-right" :class="inlineGutterClass(row.type)">{{ row.lineNumber ?? "" }}</span>
|
||||
<span class="select-none border-r border-border pr-2 text-right" :class="inlineGutterClass(row.type)">{{ row.lineNumber ?? "" }}</span>
|
||||
<span class="select-none pl-2" :class="inlineGutterClass(row.type)">{{ inlinePrefix(row.type) }}</span>
|
||||
<pre class="whitespace-pre px-2"><template v-for="(segment, index) in row.segments" :key="index"><span :class="inlineRowSegmentClass(row.type, segment.changed)">{{ segment.value }}</span></template></pre>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { Decoration, EditorView, lineNumbers, type DecorationSet } from "@codemirror/view";
|
||||
import { EditorState, StateEffect, StateField, type Range } from "@codemirror/state";
|
||||
import { basicSetup } from "codemirror";
|
||||
import { editorFontTheme, loadEditorTheme } from "@/lib/editor/editorThemes";
|
||||
import { loadNacosConfigLanguage } from "@/lib/nacos/nacosConfigLanguage";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import type { NacosSideBySideDiffRow } from "@/lib/nacos/nacosAdmin";
|
||||
|
||||
const props = defineProps<{
|
||||
rows: NacosSideBySideDiffRow[];
|
||||
side: "left" | "right";
|
||||
format: string;
|
||||
activeRow?: number | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready: [];
|
||||
}>();
|
||||
|
||||
const host = ref<HTMLElement | null>(null);
|
||||
let view: EditorView | null = null;
|
||||
let generation = 0;
|
||||
const settingsStore = useSettingsStore();
|
||||
const { isDark, themePalette } = useTheme();
|
||||
const setDecorations = StateEffect.define<DecorationSet>();
|
||||
|
||||
function currentCustomThemeColors() {
|
||||
const settings = settingsStore.editorSettings;
|
||||
if (settings.theme !== "custom") return settings.customThemeColors;
|
||||
const activeTheme = settings.customThemes?.find((theme) => theme.id === settings.activeCustomThemeId) || settings.customThemes?.[0];
|
||||
return activeTheme?.colors ?? settings.customThemeColors;
|
||||
}
|
||||
|
||||
function lineContent(row: NacosSideBySideDiffRow) {
|
||||
return props.side === "left" ? row.leftContent : row.rightContent;
|
||||
}
|
||||
|
||||
function lineType(row: NacosSideBySideDiffRow) {
|
||||
return props.side === "left" ? row.leftType : row.rightType;
|
||||
}
|
||||
|
||||
function lineNumber(row?: NacosSideBySideDiffRow) {
|
||||
if (!row) return null;
|
||||
return props.side === "left" ? row.leftLineNumber : row.rightLineNumber;
|
||||
}
|
||||
|
||||
function inlineSegments(row: NacosSideBySideDiffRow) {
|
||||
return props.side === "left" ? row.leftInline : row.rightInline;
|
||||
}
|
||||
|
||||
function buildDecorations(state: EditorState) {
|
||||
const ranges: Range<Decoration>[] = [];
|
||||
props.rows.forEach((row, index) => {
|
||||
const line = state.doc.line(index + 1);
|
||||
const type = lineType(row);
|
||||
const changeClass = type === "delete" || (type === "modify" && props.side === "left") ? "nacos-diff-line-delete" : type === "insert" || (type === "modify" && props.side === "right") ? "nacos-diff-line-insert" : type === "padding" ? "nacos-diff-line-padding" : "";
|
||||
const baseClass = changeClass;
|
||||
const classes = [baseClass, props.activeRow === index ? "nacos-diff-line-active" : ""].filter(Boolean).join(" ");
|
||||
if (classes) ranges.push(Decoration.line({ attributes: { class: classes } }).range(line.from));
|
||||
let offset = line.from;
|
||||
for (const segment of inlineSegments(row)) {
|
||||
if (segment.changed && segment.value) {
|
||||
const charClass = type === "delete" || (type === "modify" && props.side === "left") ? "nacos-diff-char-delete" : "nacos-diff-char-insert";
|
||||
ranges.push(Decoration.mark({ class: charClass }).range(offset, offset + segment.value.length));
|
||||
}
|
||||
offset += segment.value.length;
|
||||
}
|
||||
});
|
||||
return Decoration.set(ranges, true);
|
||||
}
|
||||
|
||||
const decorationsField = StateField.define<DecorationSet>({
|
||||
create: (state) => buildDecorations(state),
|
||||
update: (decorations, transaction) => {
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(setDecorations)) return effect.value;
|
||||
}
|
||||
return decorations.map(transaction.changes);
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
|
||||
async function mount() {
|
||||
const target = host.value;
|
||||
if (!target) return;
|
||||
const currentGeneration = ++generation;
|
||||
view?.destroy();
|
||||
view = null;
|
||||
const [language, theme] = await Promise.all([loadNacosConfigLanguage(props.format), loadEditorTheme(settingsStore.editorSettings.theme, isDark.value ? "dark" : "light", currentCustomThemeColors(), themePalette.value)]);
|
||||
if (currentGeneration !== generation || target !== host.value) return;
|
||||
const content = props.rows.map(lineContent).join("\n");
|
||||
const state = EditorState.create({
|
||||
doc: content,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
language,
|
||||
theme,
|
||||
editorFontTheme(EditorView, settingsStore.editorSettings.fontSize, settingsStore.editorSettings.fontFamily, { fixedHeight: true, scrollable: true }),
|
||||
lineNumbers({ formatNumber: (number) => String(lineNumber(props.rows[number - 1]) ?? "") }),
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.editable.of(false),
|
||||
decorationsField,
|
||||
EditorView.theme({
|
||||
"&": { height: "100%", backgroundColor: "transparent" },
|
||||
".cm-scroller": { overflow: "auto" },
|
||||
".cm-content": { minHeight: "100%", userSelect: "text", WebkitUserSelect: "text" },
|
||||
".cm-line": { padding: "0 8px" },
|
||||
".cm-lineNumbers .cm-gutterElement": { padding: "0 10px 0 8px" },
|
||||
}),
|
||||
],
|
||||
});
|
||||
view = new EditorView({ parent: target, state });
|
||||
emit("ready");
|
||||
}
|
||||
|
||||
function scrollToRow(index: number) {
|
||||
if (!view || index < 0 || index >= props.rows.length) return;
|
||||
view.dispatch({ effects: EditorView.scrollIntoView(view.state.doc.line(index + 1).from, { y: "center" }) });
|
||||
}
|
||||
|
||||
function scrollerElement() {
|
||||
return view?.scrollDOM;
|
||||
}
|
||||
|
||||
defineExpose({ scrollToRow, scrollerElement });
|
||||
onMounted(() => void mount());
|
||||
watch(
|
||||
() => [props.rows, props.format],
|
||||
() => void mount(),
|
||||
{ deep: true },
|
||||
);
|
||||
watch(
|
||||
() => props.activeRow,
|
||||
() => {
|
||||
if (!view) return;
|
||||
view.dispatch({ effects: setDecorations.of(buildDecorations(view.state)) });
|
||||
},
|
||||
);
|
||||
onBeforeUnmount(() => {
|
||||
generation += 1;
|
||||
view?.destroy();
|
||||
view = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="host" class="nacos-config-diff-pane h-full min-h-0 overflow-hidden rounded-sm border bg-background text-[13px] leading-6 text-foreground" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.nacos-config-diff-pane .nacos-diff-line-delete {
|
||||
background: rgb(220 38 38 / 0.2);
|
||||
}
|
||||
.nacos-config-diff-pane .nacos-diff-line-insert {
|
||||
background: rgb(22 163 74 / 0.2);
|
||||
}
|
||||
.nacos-config-diff-pane .nacos-diff-line-padding {
|
||||
background: rgb(113 113 122 / 0.15);
|
||||
}
|
||||
.dark .nacos-config-diff-pane .nacos-diff-line-padding {
|
||||
background: rgb(24 24 27 / 0.6);
|
||||
}
|
||||
.nacos-config-diff-pane .nacos-diff-line-active {
|
||||
box-shadow: inset 3px 0 0 rgb(250 204 21 / 0.95);
|
||||
}
|
||||
.nacos-config-diff-pane .nacos-diff-char-delete {
|
||||
background: rgb(248 113 113 / 0.65);
|
||||
}
|
||||
.nacos-config-diff-pane .nacos-diff-char-insert {
|
||||
background: rgb(74 222 128 / 0.6);
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { formatNacosHistoryTime } from "@/lib/nacos/nacosAdmin";
|
||||
import NacosConfigCodeViewer from "@/components/nacos/NacosConfigCodeViewer.vue";
|
||||
import type { NacosConfigHistoryItem, NacosConfigItem } from "@/types/nacos";
|
||||
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
@@ -22,6 +23,7 @@ const props = withDefaults(
|
||||
readOnly?: boolean;
|
||||
viewingItem?: NacosConfigHistoryItem | null;
|
||||
viewingContent?: string;
|
||||
viewingFormat?: string;
|
||||
viewingLoading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
@@ -31,6 +33,7 @@ const props = withDefaults(
|
||||
readOnly: false,
|
||||
viewingItem: null,
|
||||
viewingContent: "",
|
||||
viewingFormat: "",
|
||||
viewingLoading: false,
|
||||
},
|
||||
);
|
||||
@@ -208,7 +211,7 @@ function loadPage(pageNo: number) {
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
{{ t("nacos.loadingHistory") }}
|
||||
</div>
|
||||
<pre v-else class="min-h-full rounded-md border bg-background p-3 font-mono text-xs leading-5">{{ viewingContent || "" }}</pre>
|
||||
<NacosConfigCodeViewer v-else :content="viewingContent || ''" :format="viewingFormat" :data-id="viewingItem?.dataId" class="min-h-full" />
|
||||
</div>
|
||||
<DialogFooter class="m-0 shrink-0 rounded-none border-t bg-background px-5 py-5 sm:py-4">
|
||||
<Button variant="outline" @click="detailOpen = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
@@ -227,9 +230,4 @@ function loadPage(pageNo: number) {
|
||||
width: min(92vw, 1180px) !important;
|
||||
max-width: min(92vw, 1180px) !important;
|
||||
}
|
||||
|
||||
.nacos-config-history-detail-dialog pre {
|
||||
min-width: max-content;
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user