feat(elasticsearch): add custom checks and tabular REST results
This commit is contained in:
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user