fix(nacos): improve config list scrolling

* fix(nacos): improve config list scrolling

* fix(nacos): add resizable config list columns
This commit is contained in:
LSD
2026-07-10 13:40:27 +08:00
committed by GitHub
parent 7b2f969356
commit 0efc724369
3 changed files with 229 additions and 21 deletions
@@ -13,6 +13,7 @@ import EditorSearchPanel from "@/components/editor/EditorSearchPanel.vue";
import NacosConfigDiffDialog from "@/components/nacos/NacosConfigDiffDialog.vue";
import NacosConfigHistoryDialog from "@/components/nacos/NacosConfigHistoryDialog.vue";
import { useToast } from "@/composables/useToast";
import { useNacosConfigListColumnResize } from "@/composables/useNacosConfigListColumnResize";
import { useConnectionStore } from "@/stores/connectionStore";
import { useI18n } from "vue-i18n";
import * as api from "@/lib/backend/api";
@@ -113,6 +114,7 @@ const NACOS_SPLIT_SIZE_KEY = "dbx-nacos-admin-split-size";
const savedNacosSplitSize = Number(safeLocalStorageGet(NACOS_SPLIT_SIZE_KEY));
const nacosSplitSize = ref(savedNacosSplitSize >= 20 && savedNacosSplitSize <= 80 ? savedNacosSplitSize : 42);
const CONNECTION_NOT_FOUND_RETRY_DELAYS_MS = [150, 350, 700];
const { gridTemplateColumns: configListGridTemplate, minWidth: configListMinWidth, resizingColumnIndex: configListResizingColumnIndex, onResizeStart: onConfigListColumnResizeStart } = useNacosConfigListColumnResize();
const namespace = computed(() => props.namespace ?? connectionInfo.value?.namespace ?? "");
const namespaceLabel = computed(() => props.namespaceName || namespace.value || "public");
@@ -909,28 +911,43 @@ onBeforeUnmount(() => {
</div>
<div v-if="configError" class="border-b px-3 py-2 text-xs text-destructive">{{ configError }}</div>
<div class="min-h-0 flex-1 overflow-auto">
<div class="sticky top-0 z-20 grid grid-cols-4 border-b bg-muted px-3 py-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground shadow-sm">
<span class="truncate pr-3">dataID</span>
<span class="truncate pr-3">{{ t("nacos.group") }}</span>
<span class="truncate pr-3">{{ t("nacos.application") }}</span>
<span class="truncate">{{ t("nacos.format") }}</span>
<div class="min-w-max" :style="{ minWidth: configListMinWidth }">
<div class="sticky top-0 z-20 grid border-b bg-muted px-3 py-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground shadow-sm" :style="{ gridTemplateColumns: configListGridTemplate }">
<div class="relative min-w-0 truncate pr-4">
<span class="truncate">dataID</span>
<div data-column-resize-handle class="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/30" :class="configListResizingColumnIndex === 0 ? 'bg-primary/30' : ''" @mousedown="onConfigListColumnResizeStart(0, $event)" />
</div>
<div class="relative min-w-0 truncate pr-4">
<span class="truncate">{{ t("nacos.group") }}</span>
<div data-column-resize-handle class="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/30" :class="configListResizingColumnIndex === 1 ? 'bg-primary/30' : ''" @mousedown="onConfigListColumnResizeStart(1, $event)" />
</div>
<div class="relative min-w-0 truncate pr-4">
<span class="truncate">{{ t("nacos.application") }}</span>
<div data-column-resize-handle class="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/30" :class="configListResizingColumnIndex === 2 ? 'bg-primary/30' : ''" @mousedown="onConfigListColumnResizeStart(2, $event)" />
</div>
<div class="relative min-w-0 truncate pr-4">
<span class="truncate">{{ t("nacos.format") }}</span>
<div data-column-resize-handle class="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/30" :class="configListResizingColumnIndex === 3 ? 'bg-primary/30' : ''" @mousedown="onConfigListColumnResizeStart(3, $event)" />
</div>
</div>
<button
v-for="item in configs"
:key="`${item.namespace}:${item.group}:${item.dataId}`"
type="button"
class="grid w-full items-center border-b px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent/50"
:class="{ 'bg-accent': selectedConfig?.dataId === item.dataId && selectedConfig?.group === item.group }"
:style="{ gridTemplateColumns: configListGridTemplate }"
@click="selectConfig(item)"
>
<span class="flex min-w-0 items-center gap-2 pr-3" :title="item.dataId">
<FileText class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span class="truncate font-medium text-foreground">{{ item.dataId }}</span>
</span>
<span class="truncate pr-3 text-xs text-muted-foreground" :title="item.group || 'DEFAULT_GROUP'">{{ item.group || "DEFAULT_GROUP" }}</span>
<span class="truncate pr-3 text-xs text-muted-foreground" :title="item.appName || '-'">{{ item.appName || "-" }}</span>
<span class="truncate text-xs text-muted-foreground" :title="configFormatLabel(item)">{{ configFormatLabel(item) }}</span>
</button>
</div>
<button
v-for="item in configs"
:key="`${item.namespace}:${item.group}:${item.dataId}`"
type="button"
class="grid w-full grid-cols-4 items-center border-b px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent/50"
:class="{ 'bg-accent': selectedConfig?.dataId === item.dataId && selectedConfig?.group === item.group }"
@click="selectConfig(item)"
>
<span class="flex min-w-0 items-center gap-2 pr-3" :title="item.dataId">
<FileText class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span class="truncate font-medium text-foreground">{{ item.dataId }}</span>
</span>
<span class="truncate pr-3 text-xs text-muted-foreground" :title="item.group || 'DEFAULT_GROUP'">{{ item.group || "DEFAULT_GROUP" }}</span>
<span class="truncate pr-3 text-xs text-muted-foreground" :title="item.appName || '-'">{{ item.appName || "-" }}</span>
<span class="truncate text-xs text-muted-foreground" :title="configFormatLabel(item)">{{ configFormatLabel(item) }}</span>
</button>
<div v-if="!configLoading && configs.length === 0" class="flex h-full items-center justify-center text-sm text-muted-foreground">{{ t("nacos.noConfigs") }}</div>
</div>
<div class="flex shrink-0 items-center justify-between border-t px-3 py-2 text-xs text-muted-foreground">
@@ -0,0 +1,79 @@
import { computed, ref } from "vue";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
export const NACOS_CONFIG_LIST_COLUMN_WIDTHS_STORAGE_KEY = "dbx-nacos-config-list-column-widths";
export const DEFAULT_NACOS_CONFIG_LIST_COLUMN_WIDTHS = [280, 180, 180, 96] as const;
const MIN_NACOS_CONFIG_LIST_COLUMN_WIDTHS = [180, 120, 120, 72] as const;
function minWidthForColumn(index: number) {
return MIN_NACOS_CONFIG_LIST_COLUMN_WIDTHS[index] ?? MIN_NACOS_CONFIG_LIST_COLUMN_WIDTHS[MIN_NACOS_CONFIG_LIST_COLUMN_WIDTHS.length - 1];
}
function normalizeNacosConfigListColumnWidths(value: unknown): number[] | null {
if (!Array.isArray(value) || value.length !== DEFAULT_NACOS_CONFIG_LIST_COLUMN_WIDTHS.length) return null;
const widths = value.map((item) => Number(item));
if (widths.some((item) => !Number.isFinite(item))) return null;
return widths.map((width, index) => Math.max(minWidthForColumn(index), width));
}
function loadNacosConfigListColumnWidths(): number[] {
const raw = safeLocalStorageGet(NACOS_CONFIG_LIST_COLUMN_WIDTHS_STORAGE_KEY);
if (!raw) return [...DEFAULT_NACOS_CONFIG_LIST_COLUMN_WIDTHS];
try {
const normalized = normalizeNacosConfigListColumnWidths(JSON.parse(raw));
return normalized ?? [...DEFAULT_NACOS_CONFIG_LIST_COLUMN_WIDTHS];
} catch {
return [...DEFAULT_NACOS_CONFIG_LIST_COLUMN_WIDTHS];
}
}
function saveNacosConfigListColumnWidths(widths: readonly number[]) {
const normalized = normalizeNacosConfigListColumnWidths([...widths]);
if (!normalized) return;
safeLocalStorageSet(NACOS_CONFIG_LIST_COLUMN_WIDTHS_STORAGE_KEY, JSON.stringify(normalized));
}
function gridTemplateColumnsForWidths(widths: readonly number[]) {
return widths.map((width) => `${width}px`).join(" ");
}
export function useNacosConfigListColumnResize() {
const columnWidths = ref(loadNacosConfigListColumnWidths());
const resizingColumnIndex = ref<number | null>(null);
const gridTemplateColumns = computed(() => gridTemplateColumnsForWidths(columnWidths.value));
const totalWidth = computed(() => columnWidths.value.reduce((sum, width) => sum + width, 0));
const minWidth = computed(() => `${totalWidth.value}px`);
function onResizeStart(columnIndex: number, event: MouseEvent) {
if (columnIndex < 0 || columnIndex >= columnWidths.value.length) return;
event.preventDefault();
const startX = event.clientX;
const startWidth = columnWidths.value[columnIndex] ?? minWidthForColumn(columnIndex);
resizingColumnIndex.value = columnIndex;
const onMove = (moveEvent: MouseEvent) => {
const delta = moveEvent.clientX - startX;
columnWidths.value[columnIndex] = Math.max(minWidthForColumn(columnIndex), startWidth + delta);
};
const onUp = (moveEvent: MouseEvent) => {
onMove(moveEvent);
resizingColumnIndex.value = null;
saveNacosConfigListColumnWidths(columnWidths.value);
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
}
return {
columnWidths,
gridTemplateColumns,
totalWidth,
minWidth,
resizingColumnIndex,
onResizeStart,
};
}
@@ -0,0 +1,112 @@
import assert from "node:assert/strict";
import { afterEach, beforeEach, test } from "vitest";
import {
NACOS_CONFIG_LIST_COLUMN_WIDTHS_STORAGE_KEY,
DEFAULT_NACOS_CONFIG_LIST_COLUMN_WIDTHS,
useNacosConfigListColumnResize,
} from "../../apps/desktop/src/composables/useNacosConfigListColumnResize.ts";
function installLocalStorage() {
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
const store = new Map<string, string>();
Object.defineProperty(globalThis, "localStorage", {
configurable: true,
value: {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => {
store.set(key, value);
},
removeItem: (key: string) => {
store.delete(key);
},
clear: () => {
store.clear();
},
},
});
return () => {
if (original) Object.defineProperty(globalThis, "localStorage", original);
else Reflect.deleteProperty(globalThis, "localStorage");
};
}
function installDocument() {
const original = Object.getOwnPropertyDescriptor(globalThis, "document");
const listeners = new Map<string, Set<(event: MouseEvent) => void>>();
Object.defineProperty(globalThis, "document", {
configurable: true,
value: {
addEventListener: (type: string, listener: (event: MouseEvent) => void) => {
const set = listeners.get(type) ?? new Set();
set.add(listener);
listeners.set(type, set);
},
removeEventListener: (type: string, listener: (event: MouseEvent) => void) => {
listeners.get(type)?.delete(listener);
},
},
});
return {
dispatchMouseEvent(type: string, clientX: number) {
const event = { type, clientX } as MouseEvent;
listeners.get(type)?.forEach((listener) => listener(event));
},
restore() {
if (original) Object.defineProperty(globalThis, "document", original);
else Reflect.deleteProperty(globalThis, "document");
},
};
}
let restoreLocalStorage: (() => void) | undefined;
let documentHarness: ReturnType<typeof installDocument> | undefined;
beforeEach(() => {
restoreLocalStorage = installLocalStorage();
documentHarness = installDocument();
});
afterEach(() => {
documentHarness?.restore();
documentHarness = undefined;
restoreLocalStorage?.();
restoreLocalStorage = undefined;
});
test("Nacos config list keeps overflow tied to the computed width and updates it after drag resizing", () => {
const layout = useNacosConfigListColumnResize();
assert.deepEqual(layout.columnWidths.value, [...DEFAULT_NACOS_CONFIG_LIST_COLUMN_WIDTHS]);
assert.equal(layout.gridTemplateColumns.value, "280px 180px 180px 96px");
assert.equal(layout.totalWidth.value, 736);
assert.equal(layout.minWidth.value, "736px");
assert.equal(layout.totalWidth.value > 700, true);
assert.equal(layout.totalWidth.value > 820, false);
let prevented = false;
layout.onResizeStart(0, {
clientX: 280,
preventDefault() {
prevented = true;
},
} as MouseEvent);
assert.equal(prevented, true);
assert.equal(layout.resizingColumnIndex.value, 0);
documentHarness!.dispatchMouseEvent("mousemove", 400);
assert.deepEqual(layout.columnWidths.value, [400, 180, 180, 96]);
assert.equal(layout.gridTemplateColumns.value, "400px 180px 180px 96px");
assert.equal(layout.totalWidth.value, 856);
assert.equal(layout.minWidth.value, "856px");
assert.equal(layout.totalWidth.value > 820, true);
documentHarness!.dispatchMouseEvent("mouseup", 400);
assert.equal(layout.resizingColumnIndex.value, null);
assert.equal(localStorage.getItem(NACOS_CONFIG_LIST_COLUMN_WIDTHS_STORAGE_KEY), JSON.stringify([400, 180, 180, 96]));
const restoredLayout = useNacosConfigListColumnResize();
assert.deepEqual(restoredLayout.columnWidths.value, [400, 180, 180, 96]);
});