mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 05:20:02 +00:00
fix(nacos): improve config list scrolling
* fix(nacos): improve config list scrolling * fix(nacos): add resizable config list columns
This commit is contained in:
@@ -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]);
|
||||
});
|
||||
Reference in New Issue
Block a user