feat(elasticsearch): add custom checks and tabular REST results

This commit is contained in:
weiyong authored and GitHub committed 2026-07-27 18:55:37 +08:00
1 parent e2b35944ce
commit a49137ae8b
44 files changed
+917 -74

No files matched your search

@@ -0,0 +1,139 @@
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ConnectionConfig, TreeNode } from "@/types/database";
function installLocalStorage() {
const data = new Map<string, string>();
vi.stubGlobal("localStorage", {
getItem: vi.fn((key: string) => data.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
removeItem: vi.fn((key: string) => data.delete(key)),
});
}
function esConnection(): ConnectionConfig {
return {
id: "es-1",
name: "Elasticsearch",
db_type: "elasticsearch",
host: "127.0.0.1",
port: 9200,
username: "",
password: "",
database: "",
} as ConnectionConfig;
}
function seedConnectionNode(store: { treeNodes: TreeNode[]; connectedIds: Set<string> }, id = "es-1") {
store.connectedIds.add(id);
store.treeNodes.push({
id,
label: "Elasticsearch",
type: "connection",
connectionId: id,
isExpanded: false,
children: [],
});
}
describe("connectionStore Elasticsearch open/expand", () => {
beforeEach(() => {
vi.resetModules();
vi.unstubAllGlobals();
installLocalStorage();
setActivePinia(createPinia());
});
it("openElasticsearchConnectionTree only ensures connectivity, does not expand or list indices", async () => {
const elasticsearchListIndices = vi.fn().mockResolvedValue(["orders", "users"]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
elasticsearchListIndices,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.addEphemeralConnection(esConnection());
seedConnectionNode(store);
await store.openElasticsearchConnectionTree("es-1");
expect(elasticsearchListIndices).not.toHaveBeenCalled();
const node = store.treeNodes.find((n) => n.id === "es-1");
// openElasticsearchConnectionTree does NOT expand the node
expect(node?.isExpanded).toBe(false);
expect(node?.children?.some((c) => c.type === "elasticsearch-index")).toBe(false);
});
it("refreshTreeNode lists indices", async () => {
const elasticsearchListIndices = vi.fn().mockResolvedValue(["orders", "users"]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
elasticsearchListIndices,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.addEphemeralConnection(esConnection());
seedConnectionNode(store);
const node = store.treeNodes.find((n) => n.id === "es-1")!;
await store.refreshTreeNode(node);
expect(elasticsearchListIndices).toHaveBeenCalledWith("es-1");
expect(
node.children
?.filter((c) => c.type === "elasticsearch-index")
.map((c) => c.label)
.sort(),
).toEqual(["orders", "users"]);
});
it("loadElasticsearchIndices lists indices and expands", async () => {
const elasticsearchListIndices = vi.fn().mockResolvedValue(["orders", "users"]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
elasticsearchListIndices,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.addEphemeralConnection(esConnection());
seedConnectionNode(store);
await store.loadElasticsearchIndices("es-1");
expect(elasticsearchListIndices).toHaveBeenCalledWith("es-1");
const node = store.treeNodes.find((n) => n.id === "es-1");
expect(
node?.children
?.filter((c) => c.type === "elasticsearch-index")
.map((c) => c.label)
.sort(),
).toEqual(["orders", "users"]);
});
});
@@ -2324,6 +2324,7 @@ export const useConnectionStore = defineStore("connection", () => {
} else if (config.db_type === "mongodb") {
await loadMongoDatabases(connectionId);
} else if (config.db_type === "elasticsearch") {
// Reload: list indices.
await loadElasticsearchIndices(connectionId);
} else if (config.db_type === "milvus") {
await loadMilvusDatabases(connectionId);
@@ -3109,6 +3110,25 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
/**
* Connect an Elasticsearch root without expanding or listing indices.
* Used when first opening a connection (test/connect) — connectivity uses
* GET / or the configured check path via ensureConnected/test_connection.
* Expanding the node lists indices via loadElasticsearchIndices.
*/
async function openElasticsearchConnectionTree(connectionId: string) {
const node = findConnectionNode(connectionId);
if (!node) return;
// Only ensure connectivity (GET / or configured path); do not expand or list indices.
try {
await ensureConnected(connectionId);
} catch (e) {
recordMetadataLoadError(connectionId, e);
throw e;
}
}
async function loadElasticsearchIndices(connectionId: string) {
const node = findConnectionNode(connectionId);
if (!node) return;
@@ -6168,6 +6188,7 @@ export const useConnectionStore = defineStore("connection", () => {
updateRedisDbKeyStats,
loadMongoDatabases,
loadMilvusDatabases,
openElasticsearchConnectionTree,
loadElasticsearchIndices,
loadVectorCollections,
loadMongoCollections,
+20
View File
@@ -623,6 +623,25 @@ export const useQueryStore = defineStore("query", () => {
}
}
function clearResultNavigationState(tab: QueryTab) {
tab.resultSortedSql = undefined;
tab.resultSortColumn = undefined;
tab.resultSortColumnIndex = undefined;
tab.resultSortDirection = undefined;
tab.resultSortMode = undefined;
tab.resultLocalSortOriginalRows = undefined;
tab.resultLocalSortOriginalMongoDocuments = undefined;
tab.resultLocalSortOriginalMongoCopyDocuments = undefined;
tab.orderByInput = undefined;
tab.resultPageSql = undefined;
tab.resultPageLimit = undefined;
tab.resultPageOffset = undefined;
tab.resultCountSql = undefined;
tab.resultTotalRowCount = undefined;
tab.resultTotalRowCountLoading = false;
tab.resultSessionId = undefined;
}
function clearResultRunSnapshots(tab: QueryTab) {
for (const run of tab.resultRuns ?? []) {
if (run.resultCacheKey) void deleteTabResultSnapshot(run.resultCacheKey);
@@ -3510,6 +3529,7 @@ export const useQueryStore = defineStore("query", () => {
const current = tabs.value.find((item) => item.id === id);
if (current?.executionId === executionId && openInNewResultTab && current.isCancelling && restorePendingResultRun(current, executionId)) return false;
if (current?.executionId === executionId && allResults.length > 0) {
clearResultNavigationState(current);
const errorResultIndex = allResults.findIndex((result) => result.columns.includes("Error") || elasticsearchHttpErrorStatus(result) !== undefined);
const resultIndex = errorResultIndex >= 0 ? errorResultIndex : 0;
current.results = allResults.length > 1 ? allResults : undefined;