diff --git a/apps/desktop/src/components/nacos/NacosAdminConsole.vue b/apps/desktop/src/components/nacos/NacosAdminConsole.vue index 522689c24f..7f44fee450 100644 --- a/apps/desktop/src/components/nacos/NacosAdminConsole.vue +++ b/apps/desktop/src/components/nacos/NacosAdminConsole.vue @@ -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(() => {
{{ configError }}
-
- dataID - {{ t("nacos.group") }} - {{ t("nacos.application") }} - {{ t("nacos.format") }} +
+
+
+ dataID +
+
+
+ {{ t("nacos.group") }} +
+
+
+ {{ t("nacos.application") }} +
+
+
+ {{ t("nacos.format") }} +
+
+
+
-
{{ t("nacos.noConfigs") }}
diff --git a/apps/desktop/src/composables/useNacosConfigListColumnResize.ts b/apps/desktop/src/composables/useNacosConfigListColumnResize.ts new file mode 100644 index 0000000000..4577f01c35 --- /dev/null +++ b/apps/desktop/src/composables/useNacosConfigListColumnResize.ts @@ -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(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, + }; +} diff --git a/packages/app-tests/nacosConfigListLayout.test.ts b/packages/app-tests/nacosConfigListLayout.test.ts new file mode 100644 index 0000000000..dbd4101727 --- /dev/null +++ b/packages/app-tests/nacosConfigListLayout.test.ts @@ -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(); + 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 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 | 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]); +});