fix(dameng): refresh object browser after compiling views
This commit is contained in:
1 parent
28c219ff03
commit
8454560f66
6 files changed
+194
-5
No files matched your search
@@ -212,10 +212,16 @@ type DataGridHandle = DataGridColumnLayoutHandle & {
|
||||
type SearchableBrowserHandle = {
|
||||
focusSearch: (target?: Element | null) => boolean;
|
||||
refresh?: () => boolean;
|
||||
matchesRefreshScope?: (scope: ObjectBrowserRefreshScope) => boolean;
|
||||
insertCommand?: (command: string) => Promise<boolean>;
|
||||
executeCommand?: (command: string) => Promise<boolean>;
|
||||
};
|
||||
|
||||
type ObjectBrowserRefreshScope = {
|
||||
schema?: string;
|
||||
catalog?: string;
|
||||
};
|
||||
|
||||
type ElasticsearchJsonResponsePanelHandle = {
|
||||
focusSearch: () => boolean;
|
||||
};
|
||||
@@ -268,6 +274,7 @@ onMounted(() => {
|
||||
// The watcher below warms the grid for query/data tabs. Keep source-only
|
||||
// tabs out of that path: loading the grid there caused freezes (#8103).
|
||||
window.addEventListener("dbx-refresh-active-kv-browser", onRefreshActiveKvBrowser);
|
||||
window.addEventListener("dbx-refresh-object-browser", onRefreshObjectBrowser);
|
||||
window.addEventListener("resize", updateStandaloneResultToolbarDimensions);
|
||||
window.visualViewport?.addEventListener("resize", updateStandaloneResultToolbarDimensions);
|
||||
window.addEventListener("dbx:ui-scale-applied", updateStandaloneResultToolbarDimensions);
|
||||
@@ -767,6 +774,7 @@ onUnmounted(() => {
|
||||
stopRunningElapsedTimer();
|
||||
standaloneResultToolbarResizeObserver?.disconnect();
|
||||
window.removeEventListener("dbx-refresh-active-kv-browser", onRefreshActiveKvBrowser);
|
||||
window.removeEventListener("dbx-refresh-object-browser", onRefreshObjectBrowser);
|
||||
window.removeEventListener("resize", updateStandaloneResultToolbarDimensions);
|
||||
window.visualViewport?.removeEventListener("resize", updateStandaloneResultToolbarDimensions);
|
||||
window.removeEventListener("dbx:ui-scale-applied", updateStandaloneResultToolbarDimensions);
|
||||
@@ -1032,6 +1040,22 @@ function onRefreshActiveKvBrowser(event: Event) {
|
||||
void nextTick(() => refreshData());
|
||||
}
|
||||
|
||||
function matchesActiveObjectBrowserRefreshScope(detail: { connectionId?: string; database?: string } & ObjectBrowserRefreshScope): boolean {
|
||||
if (props.activeTab.mode !== "objects" || props.activeTab.connectionId !== detail.connectionId || props.activeTab.database !== detail.database) return false;
|
||||
const matchesRefreshScope = objectBrowserRef.value?.matchesRefreshScope;
|
||||
if (matchesRefreshScope) return matchesRefreshScope(detail);
|
||||
const objectBrowser = props.activeTab.objectBrowser;
|
||||
return (objectBrowser?.schema || props.activeTab.schema || "") === (detail.schema || "") && (objectBrowser?.catalog || props.activeTab.catalog || "") === (detail.catalog || "");
|
||||
}
|
||||
|
||||
function onRefreshObjectBrowser(event: Event) {
|
||||
const detail = (event as CustomEvent<{ connectionId?: string; database?: string; schema?: string; catalog?: string }>).detail;
|
||||
if (!detail || !matchesActiveObjectBrowserRefreshScope(detail)) return;
|
||||
void nextTick(() => {
|
||||
if (matchesActiveObjectBrowserRefreshScope(detail)) refreshData();
|
||||
});
|
||||
}
|
||||
|
||||
function openPluginResultView(pluginId: string, contributionId: string, label: string) {
|
||||
const result = props.activeTab.result;
|
||||
if (!result) return;
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionConfig, QueryTab } from "@/types/database";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
refresh: vi.fn(),
|
||||
schema: "APP" as string | undefined,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/editor/QueryEditor.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/grid/DataGrid.vue", () => ({ default: { render: () => null } }));
|
||||
vi.mock("@/components/objects/ObjectBrowser.vue", () => ({
|
||||
__esModule: true,
|
||||
default: defineComponent({
|
||||
props: { catalog: String },
|
||||
setup(props, { expose }) {
|
||||
expose({
|
||||
focusSearch: () => false,
|
||||
refresh: mocks.refresh,
|
||||
matchesRefreshScope: (scope: { schema?: string; catalog?: string }) => (mocks.schema || "") === (scope.schema || "") && (props.catalog || "") === (scope.catalog || ""),
|
||||
});
|
||||
return () => h("div", { "data-test": "object-browser" });
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
import ContentArea from "../ContentArea.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
|
||||
const mountedApps: Array<{ app: App; host: HTMLElement }> = [];
|
||||
|
||||
const connection = {
|
||||
id: "dameng",
|
||||
name: "Dameng",
|
||||
db_type: "dameng",
|
||||
host: "localhost",
|
||||
port: 5236,
|
||||
username: "SYSDBA",
|
||||
password: "",
|
||||
} as ConnectionConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.refresh.mockReset();
|
||||
mocks.refresh.mockReturnValue(true);
|
||||
mocks.schema = "APP";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const { app, host } of mountedApps.splice(0)) {
|
||||
app.unmount();
|
||||
host.remove();
|
||||
}
|
||||
window.localStorage?.clear();
|
||||
});
|
||||
|
||||
async function mountObjectBrowserTab() {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
useConnectionStore().connections = [connection];
|
||||
const tab = reactive<QueryTab>({
|
||||
id: "objects",
|
||||
title: "app objects",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
mode: "objects",
|
||||
sql: "",
|
||||
isExecuting: false,
|
||||
});
|
||||
useQueryStore().tabs.push(tab);
|
||||
const host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
const app = createApp({
|
||||
setup: () => () =>
|
||||
h(ContentArea, {
|
||||
activeTab: tab,
|
||||
activeConnection: connection,
|
||||
activeOutputView: "result",
|
||||
executableSql: "",
|
||||
formatSqlRequest: null,
|
||||
compressSqlRequest: null,
|
||||
selectedSql: "",
|
||||
cursorPos: 0,
|
||||
blockDangerousRedisCommands: false,
|
||||
}),
|
||||
});
|
||||
app.use(pinia);
|
||||
app.use(createI18n({ legacy: false, locale: "en", messages: { en: {} }, missingWarn: false, fallbackWarn: false }));
|
||||
app.mount(host);
|
||||
mountedApps.push({ app, host });
|
||||
await vi.waitFor(() => expect(host.querySelector('[data-test="object-browser"]')).not.toBeNull());
|
||||
return { tab };
|
||||
}
|
||||
|
||||
function dispatchRefresh(detail: { connectionId?: string; database?: string; schema?: string; catalog?: string }) {
|
||||
window.dispatchEvent(new CustomEvent("dbx-refresh-object-browser", { detail }));
|
||||
}
|
||||
|
||||
describe("ContentArea object browser refresh notifications", () => {
|
||||
it("refreshes an object tab using the schema selected inside ObjectBrowser", async () => {
|
||||
await mountObjectBrowserTab();
|
||||
|
||||
dispatchRefresh({ connectionId: connection.id, database: "app", schema: "APP" });
|
||||
await nextTick();
|
||||
|
||||
expect(mocks.refresh).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ connectionId: "other", database: "app", schema: "APP" },
|
||||
{ connectionId: connection.id, database: "other", schema: "APP" },
|
||||
{ connectionId: connection.id, database: "app", schema: "OTHER" },
|
||||
{ connectionId: connection.id, database: "app", schema: "APP", catalog: "OTHER" },
|
||||
])("ignores a refresh outside the active browser scope: %o", async (detail) => {
|
||||
await mountObjectBrowserTab();
|
||||
|
||||
dispatchRefresh(detail);
|
||||
await nextTick();
|
||||
|
||||
expect(mocks.refresh).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rechecks the ObjectBrowser scope before the deferred refresh", async () => {
|
||||
await mountObjectBrowserTab();
|
||||
|
||||
dispatchRefresh({ connectionId: connection.id, database: "app", schema: "APP" });
|
||||
mocks.schema = "OTHER";
|
||||
await nextTick();
|
||||
|
||||
expect(mocks.refresh).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -3102,7 +3102,11 @@ function clearObjectSearch() {
|
||||
getSearchInput()?.focus();
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, refresh });
|
||||
function matchesRefreshScope(scope: { schema?: string; catalog?: string }): boolean {
|
||||
return (selectedSchema.value || "") === (scope.schema || "") && (props.catalog || "") === (scope.catalog || "");
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, refresh, matchesRefreshScope });
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
objectBrowserRowsLoadGuard.invalidate();
|
||||
|
||||
+18
-3
@@ -8,13 +8,14 @@ import type { ConnectionConfig, ObjectInfo } from "@/types/database";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listObjects: vi.fn(),
|
||||
listSchemas: vi.fn(),
|
||||
ensureConnected: vi.fn(),
|
||||
editorInstances: 0,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
listObjects: (...args: unknown[]) => mocks.listObjects(...args),
|
||||
listSchemas: vi.fn().mockResolvedValue([]),
|
||||
listSchemas: (...args: unknown[]) => mocks.listSchemas(...args),
|
||||
listObjectStatistics: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
@@ -81,6 +82,7 @@ const mountedApps: Array<{ app: App; host: HTMLElement }> = [];
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.listObjects.mockResolvedValue([]);
|
||||
mocks.listSchemas.mockResolvedValue([]);
|
||||
mocks.ensureConnected.mockResolvedValue(undefined);
|
||||
mocks.editorInstances = 0;
|
||||
invalidateObjectBrowserRowsCache({});
|
||||
@@ -96,14 +98,15 @@ afterEach(() => {
|
||||
|
||||
async function mountBrowser(overrides: Partial<InstanceType<typeof ObjectBrowser>["$props"]> = {}) {
|
||||
const props = reactive({ connection, database: "app", initialEventCreateRequestId: 1 as number | undefined, ...overrides });
|
||||
const browser = ref<InstanceType<typeof ObjectBrowser> | null>(null);
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp({ setup: () => () => h(ObjectBrowser, props) });
|
||||
const app = createApp({ setup: () => () => h(ObjectBrowser, { ...props, ref: browser }) });
|
||||
mountedApps.push({ app, host });
|
||||
app.mount(host);
|
||||
await vi.waitFor(() => expect(mocks.listObjects).toHaveBeenCalledOnce());
|
||||
await nextTick();
|
||||
return { host, props };
|
||||
return { browser, host, props };
|
||||
}
|
||||
|
||||
describe("ObjectBrowser event editor rendering", () => {
|
||||
@@ -184,3 +187,15 @@ describe("ObjectBrowser event editor rendering", () => {
|
||||
expect(host.querySelector("[data-event-editor]")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ObjectBrowser refresh scope", () => {
|
||||
it("matches the schema selected automatically by loadSchemas", async () => {
|
||||
mocks.listSchemas.mockResolvedValue(["APP", "OTHER"]);
|
||||
const damengConnection = { ...connection, id: "dameng", name: "Dameng", db_type: "dameng" } as ConnectionConfig;
|
||||
const { browser } = await mountBrowser({ connection: damengConnection, initialEventCreateRequestId: undefined });
|
||||
|
||||
expect(browser.value?.matchesRefreshScope({ schema: "APP" })).toBe(true);
|
||||
expect(browser.value?.matchesRefreshScope({ schema: "OTHER" })).toBe(false);
|
||||
expect(browser.value?.matchesRefreshScope({ schema: "APP", catalog: "OTHER" })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2631,6 +2631,16 @@ async function compileDamengView() {
|
||||
if (!executed) return;
|
||||
toast(t("contextMenu.compileObjectSuccess", { name: node.label }), 3000);
|
||||
await connectionStore.refreshObjectListTreeNode(node.connectionId, node.database, node.schema);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("dbx-refresh-object-browser", {
|
||||
detail: {
|
||||
connectionId: node.connectionId,
|
||||
database: node.database,
|
||||
schema: node.schema,
|
||||
catalog: node.catalog,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (e: any) {
|
||||
compileErrorTitle.value = t("contextMenu.compileObjectFailedTitle");
|
||||
compileErrorMessage.value = t("contextMenu.compileObjectFailedMessage", { name: node.label, message: e?.message || String(e) });
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ describe("ContentArea object browser refresh wiring", () => {
|
||||
|
||||
it("exposes object and active table-info reloads as refresh", () => {
|
||||
expect(objectBrowserSource).toMatch(/function refresh\(\): boolean \{\s+void reload\(\{ preserveExistingRows: true \}\);\s+void refreshActiveTableInfo\(\);\s+return true;\s+\}/);
|
||||
expect(objectBrowserSource).toContain("defineExpose({ focusSearch, refresh });");
|
||||
expect(objectBrowserSource).toContain("defineExpose({ focusSearch, refresh, matchesRefreshScope });");
|
||||
});
|
||||
|
||||
it("shows the configured content refresh shortcut on the refresh button", () => {
|
||||
|
||||
Reference in new issue
Block a user