feat(elasticsearch): show index aliases on sidebar rows

This commit is contained in:
onenewcode authored and GitHub committed 2026-09-11 13:24:41 +08:00
1 parent 6299d62125
commit 61bd3cdf62
22 files changed
+432 -51

No files matched your search

@@ -90,6 +90,7 @@ import { focusSidebarRenameInput } from "@/lib/sidebar/sidebarRenameFocus";
import { ensureSqlExtension, stripSqlExtension } from "@/lib/savedSql/savedSqlFileName";
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { elasticsearchIndexAliasLabel } from "@/lib/sidebar/elasticsearchIndexActions";
import { isXuguPublicSynonymTreeNode, isXuguSchedulerJobTreeNode, xuguSchemaDisplayName } from "@/lib/sidebar/xuguPublicSynonyms";
import { xuguDatafileDetailRows, xuguTablespaceDetailRows } from "@/lib/sidebar/xuguTablespaces";
// --- Drag and Drop ---
@@ -442,6 +443,7 @@ function treeNodeSecondaryValue(node: TreeNode): string | undefined {
if (node.type === "type" && node.customTypeKind) return t(`customType.kinds.${node.customTypeKind}`);
if (node.type === "type-member") return (node.meta as CustomTypeTreeMemberMeta | undefined)?.displayValue;
if (node.type === "datafile") return node.xuguDatafilePath;
if (node.type === "elasticsearch-index") return elasticsearchIndexAliasLabel(node);
return undefined;
}
@@ -604,6 +606,16 @@ const detailTooltip = computed(() => {
}));
return rows.length ? { rows } : null;
}
if (node.type === "elasticsearch-index") {
const aliases = elasticsearchIndexAliasLabel(node);
if (!aliases) return null;
return {
rows: [
{ label: t("objects.name"), value: visibleLabel(node) },
{ label: t("tree.elasticsearchAlias"), value: aliases },
],
};
}
const comment = node.type === "column" && node.meta && "comment" in node.meta ? (node.meta as ColumnInfo).comment : node.comment;
if (!comment || (node.type !== "schema" && node.type !== "table" && node.type !== "view" && node.type !== "column")) return null;
const rows: DetailTooltipRow[] = [
@@ -1567,7 +1579,10 @@ function onKeydown(event: KeyboardEvent) {
]"
>{{ visibleLabel(node) }}</span
>
<span v-if="treeNodeSecondaryValue(node)" class="min-w-0 max-w-[55%] shrink truncate text-xs text-muted-foreground" :title="treeNodeSecondaryValue(node)">{{ treeNodeSecondaryValue(node) }}</span>
<span v-if="treeNodeSecondaryValue(node)" class="flex min-w-0 max-w-[55%] shrink items-center gap-1 text-xs text-muted-foreground" :title="node.type === 'elasticsearch-index' ? undefined : treeNodeSecondaryValue(node)">
<Link2 v-if="node.type === 'elasticsearch-index'" class="h-3 w-3 shrink-0 text-sky-400" />
<span class="min-w-0 truncate">{{ treeNodeSecondaryValue(node) }}</span>
</span>
<button
v-if="canDragPinnedOrder()"
type="button"
+1
View File
@@ -3637,6 +3637,7 @@ export default withEnglishFallback({
},
linkedServers: "Əlaqələndirilmiş serverlər",
defaultDatabase: "Standart verilənlər bazası",
elasticsearchAlias: "Təxəllüs",
columns: "Sütunlar",
attributes: "Atributlar",
methods: "Metodlar",
+1
View File
@@ -3636,6 +3636,7 @@ export default {
},
linkedServers: "Linked Servers",
defaultDatabase: "Default DB",
elasticsearchAlias: "Alias",
columns: "Columns",
attributes: "Attributes",
methods: "Methods",
+1
View File
@@ -3518,6 +3518,7 @@ export default withEnglishFallback({
},
linkedServers: "Servidores vinculados",
defaultDatabase: "Base predeterminada",
elasticsearchAlias: "Alias",
columns: "Columnas",
attributes: "Atributos",
methods: "Métodos",
+1
View File
@@ -3516,6 +3516,7 @@ export default withEnglishFallback({
},
linkedServers: "Server Collegati",
defaultDatabase: "DB Predefinito",
elasticsearchAlias: "Alias",
columns: "Colonne",
attributes: "Attributi",
methods: "Metodi",
+1
View File
@@ -3541,6 +3541,7 @@ export default withEnglishFallback({
no: "いいえ",
},
defaultDatabase: "デフォルトDB",
elasticsearchAlias: "エイリアス",
columns: "列",
attributes: "属性",
methods: "メソッド",
+1
View File
@@ -3395,6 +3395,7 @@ export default withEnglishFallback({
},
linkedServers: "연결된 서버",
defaultDatabase: "기본 DB",
elasticsearchAlias: "별칭",
columns: "컬럼",
attributes: "속성",
methods: "메서드",
+1
View File
@@ -3519,6 +3519,7 @@ export default withEnglishFallback({
linkedServers: "Servidores Vinculados",
materializedViews: "Visualizações Materializadas",
defaultDatabase: "Banco padrão",
elasticsearchAlias: "Alias",
columns: "Colunas",
attributes: "Atributos",
methods: "Métodos",
+1
View File
@@ -3609,6 +3609,7 @@ export default withEnglishFallback({
},
linkedServers: "Bağlı Sunucular",
defaultDatabase: "Varsayılan VT",
elasticsearchAlias: "Takma ad",
columns: "Sütunlar",
attributes: "Öznitelikler",
methods: "Yöntemler",
+1
View File
@@ -3557,6 +3557,7 @@ export default withEnglishFallback({
},
linkedServers: "链接服务器",
defaultDatabase: "默认库",
elasticsearchAlias: "别名",
columns: "字段",
attributes: "属性",
methods: "方法",
+1
View File
@@ -3516,6 +3516,7 @@ export default withEnglishFallback({
linkedServers: "連結伺服器",
materializedViews: "具體化檢視",
defaultDatabase: "預設庫",
elasticsearchAlias: "別名",
columns: "欄位",
attributes: "屬性",
methods: "方法",
+1 -1
View File
@@ -3863,7 +3863,7 @@ export async function mongoCloneCollection(connectionId: string, database: strin
export async function elasticsearchListIndices(connectionId: string): Promise<string[]> {
const collections = await documentListCollections(connectionId, "default");
return collections.map((c) => c.name);
return [...new Set(collections.flatMap((collection) => [collection.name, ...(collection.aliases ?? [])].filter((name) => name.trim())))];
}
/** Lists every Meilisearch index visible to the current connection credentials. */
+1 -1
View File
@@ -3951,7 +3951,7 @@ export async function vectorRenameCollection(connectionId: string, database: str
export async function elasticsearchListIndices(connectionId: string): Promise<string[]> {
const collections = await documentListCollections(connectionId, "default");
return collections.map((c) => c.name);
return [...new Set(collections.flatMap((collection) => [collection.name, ...(collection.aliases ?? [])].filter((name) => name.trim())))];
}
/** Lists every Meilisearch index visible to the current connection credentials. */
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { elasticsearchClearIndexPreview, isElasticsearchClearConfirmed, isElasticsearchIndexPattern, isElasticsearchProtocolIndex, isPartialElasticsearchClear, matchesElasticsearchIndexPattern } from "@/lib/sidebar/elasticsearchIndexActions";
import { elasticsearchClearIndexPreview, elasticsearchIndexAliasLabel, isElasticsearchClearConfirmed, isElasticsearchIndexPattern, isElasticsearchProtocolIndex, isPartialElasticsearchClear, matchesElasticsearchIndexPattern } from "@/lib/sidebar/elasticsearchIndexActions";
import type { ElasticsearchDeleteByQueryResult } from "@/lib/backend/tauri";
function clearResult(overrides: Partial<ElasticsearchDeleteByQueryResult> = {}): ElasticsearchDeleteByQueryResult {
@@ -16,6 +16,12 @@ describe("Elasticsearch index actions", () => {
expect(isElasticsearchProtocolIndex("elasticsearch-index", undefined)).toBe(false);
});
it("renders aliases on the same row as the index name", () => {
expect(elasticsearchIndexAliasLabel({ searchAliases: ["orders-write", "orders-read"] })).toBe("orders-write, orders-read");
expect(elasticsearchIndexAliasLabel({ searchAliases: ["", " "] })).toBeUndefined();
expect(elasticsearchIndexAliasLabel({})).toBeUndefined();
});
it("previews the exact request the clear action sends", () => {
const preview = elasticsearchClearIndexPreview("logs-2026.08");
expect(preview).toContain("POST /logs-2026.08/_delete_by_query?conflicts=proceed&refresh=true");
@@ -1,4 +1,4 @@
import type { DatabaseType } from "@/types/database";
import type { DatabaseType, TreeNode } from "@/types/database";
import type { ElasticsearchDeleteByQueryResult } from "@/lib/backend/tauri";
/**
@@ -10,6 +10,11 @@ export function isElasticsearchProtocolIndex(nodeType: string, dbType: DatabaseT
return nodeType === "elasticsearch-index" && (dbType === "elasticsearch" || dbType === "easysearch");
}
export function elasticsearchIndexAliasLabel(node: Pick<TreeNode, "searchAliases">): string | undefined {
const aliases = node.searchAliases?.filter((alias) => alias.trim()) ?? [];
return aliases.length ? aliases.join(", ") : undefined;
}
/**
* With an `index_grouping` regex configured, a sidebar index node is a collapsed
* pattern (`logs-2026.08.*`) rather than one index, and clearing it hits every
@@ -45,13 +45,17 @@ describe("connectionStore Elasticsearch open/expand", () => {
});
it("openElasticsearchConnectionTree only ensures connectivity, does not expand or list indices", async () => {
const elasticsearchListIndices = vi.fn().mockResolvedValue(["orders", "users"]);
const documentListCollections = vi.fn().mockResolvedValue([
{ name: "orders", id: "orders", kind: "index" },
{ name: "users", id: "users", kind: "index" },
]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
elasticsearchListIndices,
documentListCollections,
elasticsearchListIndices: vi.fn(),
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
@@ -66,7 +70,7 @@ describe("connectionStore Elasticsearch open/expand", () => {
await store.openElasticsearchConnectionTree("es-1");
expect(elasticsearchListIndices).not.toHaveBeenCalled();
expect(documentListCollections).not.toHaveBeenCalled();
const node = store.treeNodes.find((n) => n.id === "es-1");
// openElasticsearchConnectionTree does NOT expand the node
expect(node?.isExpanded).toBe(false);
@@ -74,13 +78,17 @@ describe("connectionStore Elasticsearch open/expand", () => {
});
it("refreshTreeNode lists indices", async () => {
const elasticsearchListIndices = vi.fn().mockResolvedValue(["orders", "users"]);
const documentListCollections = vi.fn().mockResolvedValue([
{ name: "orders", id: "orders", kind: "index" },
{ name: "users", id: "users", kind: "index" },
]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
elasticsearchListIndices,
documentListCollections,
elasticsearchListIndices: vi.fn(),
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
@@ -96,7 +104,7 @@ describe("connectionStore Elasticsearch open/expand", () => {
await store.refreshTreeNode(node);
expect(elasticsearchListIndices).toHaveBeenCalledWith("es-1");
expect(documentListCollections).toHaveBeenCalledWith("es-1", "default");
expect(
node.children
?.filter((c) => c.type === "elasticsearch-index")
@@ -106,13 +114,17 @@ describe("connectionStore Elasticsearch open/expand", () => {
});
it("loadElasticsearchIndices lists indices and expands", async () => {
const elasticsearchListIndices = vi.fn().mockResolvedValue(["orders", "users"]);
const documentListCollections = vi.fn().mockResolvedValue([
{ name: "orders", id: "orders", kind: "index" },
{ name: "users", id: "users", kind: "index" },
]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
elasticsearchListIndices,
documentListCollections,
elasticsearchListIndices: vi.fn(),
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
@@ -127,7 +139,7 @@ describe("connectionStore Elasticsearch open/expand", () => {
await store.loadElasticsearchIndices("es-1");
expect(elasticsearchListIndices).toHaveBeenCalledWith("es-1");
expect(documentListCollections).toHaveBeenCalledWith("es-1", "default");
const node = store.treeNodes.find((n) => n.id === "es-1");
expect(
node?.children
@@ -138,13 +150,17 @@ describe("connectionStore Elasticsearch open/expand", () => {
});
it("loads Easysearch indices through the Elasticsearch-compatible tree", async () => {
const elasticsearchListIndices = vi.fn().mockResolvedValue(["orders", "users"]);
const documentListCollections = vi.fn().mockResolvedValue([
{ name: "orders", id: "orders", kind: "index" },
{ name: "users", id: "users", kind: "index" },
]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
elasticsearchListIndices,
documentListCollections,
elasticsearchListIndices: vi.fn(),
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
@@ -159,16 +175,56 @@ describe("connectionStore Elasticsearch open/expand", () => {
await store.loadElasticsearchIndices("easysearch-1");
expect(elasticsearchListIndices).toHaveBeenCalledWith("easysearch-1");
expect(documentListCollections).toHaveBeenCalledWith("easysearch-1", "default");
expect(
store.treeNodes
.find((node) => node.id === "easysearch-1")
?.children?.map((node) => node.label)
?.children?.filter((node) => node.type === "elasticsearch-index")
.map((node) => node.label)
.sort(),
).toEqual(["orders", "users"]);
});
it("keeps aliases on the same index row instead of adding a second node", async () => {
const documentListCollections = vi.fn().mockResolvedValue([
{ name: "orders", id: "orders", aliases: ["orders-write"] },
{ name: "users", id: "users", aliases: [] },
]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
documentListCollections,
elasticsearchListIndices: vi.fn(),
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");
const nodes = store.treeNodes.find((node) => node.id === "es-1")?.children?.filter((node) => node.type === "elasticsearch-index");
expect(
nodes?.map((node) => ({
label: node.label,
searchAliases: node.searchAliases,
})),
).toEqual([
{ label: "orders", searchAliases: ["orders-write"] },
{ label: "users", searchAliases: undefined },
]);
});
it("uses the shared Meilisearch index-list method for the Meilisearch tree", async () => {
const documentListCollections = vi.fn().mockResolvedValue([{ name: "wrong-index", id: "wrong-index", kind: "index" }]);
const elasticsearchListIndices = vi.fn().mockResolvedValue(["wrong-index"]);
const meilisearchListIndexes = vi.fn().mockResolvedValue(["movies", "books"]);
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
@@ -176,6 +232,7 @@ describe("connectionStore Elasticsearch open/expand", () => {
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth,
documentListCollections,
elasticsearchListIndices,
meilisearchListIndexes,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
@@ -193,6 +250,7 @@ describe("connectionStore Elasticsearch open/expand", () => {
await store.loadElasticsearchIndices("meili-1");
expect(meilisearchListIndexes).toHaveBeenCalledWith("meili-1");
expect(documentListCollections).not.toHaveBeenCalled();
expect(elasticsearchListIndices).not.toHaveBeenCalled();
expect(
store.treeNodes
+16 -9
View File
@@ -4989,7 +4989,21 @@ export const useConnectionStore = defineStore("connection", () => {
await ensureConnected(connectionId);
load = reclaimTreeNodeLoad(load, node);
const isMeilisearch = getConfig(connectionId)?.db_type === "meilisearch";
const indices = await withMetadataLoadTimeout(connectionId, isMeilisearch ? api.meilisearchListIndexes(connectionId) : api.elasticsearchListIndices(connectionId), isMeilisearch ? "Meilisearch indexes" : "Elasticsearch indices");
const collections = isMeilisearch
? sortSidebarNames(await withMetadataLoadTimeout(connectionId, api.meilisearchListIndexes(connectionId), "Meilisearch indexes")).map((name) => ({ name, aliases: [] as string[] }))
: [...(await withMetadataLoadTimeout(connectionId, api.documentListCollections(connectionId, "default"), "Elasticsearch indices"))].sort((left, right) => compareSidebarNames(left.name, right.name));
const indexNodes = collections.map((collection) => {
const aliases = collection.aliases?.filter((alias) => alias.trim());
return {
id: `${connectionId}:__collection:${collection.name}`,
label: collection.name,
type: "elasticsearch-index" as const,
connectionId,
database: "default",
isExpanded: false,
...(aliases?.length ? { searchAliases: aliases } : {}),
};
});
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
setChildren(
@@ -4997,14 +5011,7 @@ export const useConnectionStore = defineStore("connection", () => {
withSavedSqlRoot(
connectionId,
[
...sortSidebarNames(indices).map((index) => ({
id: `${connectionId}:__collection:${index}`,
label: index,
type: "elasticsearch-index" as const,
connectionId,
database: "default",
isExpanded: false,
})),
...indexNodes,
...(isMeilisearch
? [
{
+1
View File
@@ -1562,4 +1562,5 @@ export interface CollectionInfo {
milvusSchema?: MilvusCollectionSchema;
kind?: MongoCollectionKind | "bucket";
bucketName?: string;
aliases?: string[];
}