feat(elasticsearch): model indices as first-class nodes

This commit is contained in:
t8y2 2026-06-11 12:52:47 +08:00
parent 6f253abe55
commit 26d714909e
15 changed files with 146 additions and 38 deletions

View File

@ -5272,15 +5272,18 @@ function toggleCellDetailPanelLayout() {
const tableMetadataCapabilities = computed(() => getTableMetadataCapabilities(props.databaseType));
const tableInfoTabs = computed(() => {
const tabs: TableInfoTabItem[] = [
{
const tabs: TableInfoTabItem[] = [];
if (tableMetadataCapabilities.value.columns) {
tabs.push({
id: "columns",
label: t("grid.tableInfoColumns"),
icon: ListTree,
count: props.tableMeta?.columns.length,
},
{ id: "indexes", label: t("grid.tableInfoIndexes"), icon: KeyRound, count: indexes.value.length },
];
});
}
if (tableMetadataCapabilities.value.indexes) {
tabs.push({ id: "indexes", label: t("grid.tableInfoIndexes"), icon: KeyRound, count: indexes.value.length });
}
if (tableMetadataCapabilities.value.foreignKeys) {
tabs.push({
id: "foreignKeys",
@ -5292,7 +5295,9 @@ const tableInfoTabs = computed(() => {
if (tableMetadataCapabilities.value.triggers) {
tabs.push({ id: "triggers", label: t("grid.tableInfoTriggers"), icon: RotateCcw, count: triggers.value.length });
}
tabs.push({ id: "ddl", label: "DDL", icon: Code2 });
if (tableMetadataCapabilities.value.ddl) {
tabs.push({ id: "ddl", label: "DDL", icon: Code2 });
}
return tabs;
});
const tableInfoTabListStyle = computed(() => ({
@ -5309,7 +5314,8 @@ async function toggleTableInfo(tab: TableInfoTab = activeTableInfoTab.value) {
}
async function selectTableInfoTab(tab: TableInfoTab) {
const nextTab = tableInfoTabs.value.some((item) => item.id === tab) ? tab : "columns";
const nextTab = tableInfoTabs.value.some((item) => item.id === tab) ? tab : tableInfoTabs.value[0]?.id;
if (!nextTab) return;
activeTableInfoTab.value = nextTab;
if (nextTab === "ddl") await fetchDdl();
else if (nextTab === "indexes") await fetchIndexes();

View File

@ -820,10 +820,10 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
</div>
</template>
<!-- MongoDB mode: document browser -->
<!-- Document mode: MongoDB collections and Elasticsearch indices -->
<template v-else-if="activeTab.mode === 'mongo'">
<div class="flex-1 min-h-0">
<MongoDocBrowser :key="activeTab.id" :connection-id="activeTab.connectionId" :database="activeTab.database" :collection="activeTab.sql" />
<MongoDocBrowser :key="activeTab.id" :connection-id="activeTab.connectionId" :database="activeTab.database" :collection="activeTab.sql" :database-type="activeEffectiveDatabaseType" />
</div>
</template>

View File

@ -16,7 +16,7 @@ import { normalizeResultPageSize } from "@/lib/paginationPageSize";
import { useSettingsStore } from "@/stores/settingsStore";
import JsonEditNode from "./JsonEditNode.vue";
import type { EditNode } from "@/types/editor";
import type { QueryResult } from "@/types/database";
import type { DatabaseType, QueryResult } from "@/types/database";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
@ -27,6 +27,7 @@ const props = defineProps<{
connectionId: string;
database: string;
collection: string;
databaseType?: DatabaseType;
}>();
type JsonRecord = Record<string, unknown>;
@ -64,6 +65,17 @@ const tableFindPaneStyle = computed(() => {
if (tableFindPaneWidth.value == null) return {};
return { flex: `0 0 ${tableFindPaneWidth.value}px` };
});
const documentStoreLabels = computed(() =>
props.databaseType === "elasticsearch"
? {
documentsLabel: "Documents",
queryPreview: `${props.collection}/_search`,
}
: {
documentsLabel: t("mongo.documents", { count: total.value }),
queryPreview: mongoQueryPreview.value,
},
);
type PendingDelete = { kind: "document"; index: number } | { kind: "field"; index: number; name: string };
type LocalFilterSummary = {
@ -114,6 +126,7 @@ const deleteDetails = computed(() => {
if (!pending) return "";
if (pending.kind === "document") {
const id = documents.value[pending.index]?._id ?? "";
if (props.databaseType === "elasticsearch") return `Elasticsearch index: ${props.collection}\nDocument _id: ${String(id)}`;
return t("dangerDialog.mongoDocumentDetails", { collection: props.collection, id: String(id) });
}
return t("dangerDialog.mongoFieldDetails", { field: pending.name || t("mongo.field") });
@ -675,7 +688,7 @@ function resetTableSearchSplitWidth() {
</Button>
</div>
<span class="shrink-0 ml-1">{{ t("mongo.documents", { count: total }) }}</span>
<span class="shrink-0 ml-1">{{ documentStoreLabels.documentsLabel }}</span>
<Button v-if="viewMode === 'document'" variant="ghost" size="icon" class="h-5 w-5" @click="startNew"><Plus class="h-3 w-3" /></Button>
<Button v-if="viewMode === 'document'" variant="ghost" size="icon" class="h-5 w-5" @click="load"><RefreshCw class="h-3 w-3" :class="{ 'animate-spin': loading }" /></Button>
@ -767,7 +780,7 @@ function resetTableSearchSplitWidth() {
editable
:custom-save="gridSave"
:loading="loading"
:sql="mongoQueryPreview"
:sql="documentStoreLabels.queryPreview"
:page-offset="page * pageSize"
:page-limit="pageSize"
:total-row-count="total"

View File

@ -57,7 +57,7 @@ const SEARCH_SCOPE_TO_NODE_TYPES: Record<SearchScope, TreeNodeType[]> = {
connection: ["connection"],
database: ["database", "redis-db", "mongo-db"],
schema: ["schema"],
table: ["table", "mongo-collection"],
table: ["table", "mongo-collection", "elasticsearch-index"],
view: ["view"],
};
@ -275,8 +275,10 @@ async function ensureTreeLoadedForTab(tab: QueryTab, opts?: { force?: boolean })
try {
if (config.db_type === "redis") {
await store.loadRedisDatabases(connId);
} else if (config.db_type === "mongodb" || config.db_type === "elasticsearch") {
} else if (config.db_type === "mongodb") {
await store.loadMongoDatabases(connId);
} else if (config.db_type === "elasticsearch") {
await store.loadElasticsearchIndices(connId);
} else {
await store.loadDatabases(connId, loadOptions);
}

View File

@ -219,6 +219,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: Database, colorClass: "text-yellow-500" };
case "mongo-collection":
return { icon: Table, colorClass: "text-green-400" };
case "elasticsearch-index":
return { icon: Table, colorClass: "text-emerald-400" };
case "procedure":
return { icon: ScrollText, colorClass: "text-blue-500" };
case "function":
@ -249,7 +251,7 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
}
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-tables", "group-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions"]);
const pinnableTypes: Set<TreeNodeType> = new Set(["connection-group", "database", "schema", "table", "view", "redis-db", "mongo-db", "mongo-collection"]);
const pinnableTypes: Set<TreeNodeType> = new Set(["connection-group", "database", "schema", "table", "view", "redis-db", "mongo-db", "mongo-collection", "elasticsearch-index"]);
function isGroupLabel(node: TreeNode): boolean {
return groupTypes.has(node.type);
@ -263,7 +265,7 @@ function displayLabel(node: TreeNode): string {
}
function visibleLabel(node: TreeNode): string {
if (node.type === "table" || node.type === "view" || node.type === "mongo-collection") {
if (node.type === "table" || node.type === "view" || node.type === "mongo-collection" || node.type === "elasticsearch-index") {
return sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes);
}
return displayLabel(node);
@ -312,8 +314,10 @@ async function toggle() {
await connectionStore.loadRedisDatabases(node.connectionId);
} else if (config?.db_type === "etcd") {
await connectionStore.loadEtcdRoot(node.connectionId);
} else if (config?.db_type === "mongodb" || config?.db_type === "elasticsearch") {
} else if (config?.db_type === "mongodb") {
await connectionStore.loadMongoDatabases(node.connectionId);
} else if (config?.db_type === "elasticsearch") {
await connectionStore.loadElasticsearchIndices(node.connectionId);
} else {
await connectionStore.loadDatabases(node.connectionId);
}
@ -331,6 +335,9 @@ async function toggle() {
const tabTitle = `${node.database}.${node.label}`;
const tab = queryStore.createTab(node.connectionId, node.database, tabTitle, "mongo");
queryStore.updateSql(tab, node.label);
} else if (node.type === "elasticsearch-index" && node.connectionId) {
const tab = queryStore.createTab(node.connectionId, node.database || "default", node.label, "mongo");
queryStore.updateSql(tab, node.label);
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
const config = connectionStore.getConfig(node.connectionId);
if (config?.db_type === "sqlserver") {
@ -2237,7 +2244,7 @@ const hasTypeMenu = computed(() => {
return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || t === "column" || t === "procedure" || t === "function" || t === "package" || t === "package-body" || isGroupLabel(props.node);
});
const columnComment = computed(() => (props.node.type === "column" && props.node.meta && "comment" in props.node.meta ? (props.node.meta as any).comment : null));
const tableComment = computed(() => ((props.node.type === "table" || props.node.type === "view" || props.node.type === "mongo-collection") && props.node.comment ? props.node.comment : null));
const tableComment = computed(() => ((props.node.type === "table" || props.node.type === "view" || props.node.type === "mongo-collection" || props.node.type === "elasticsearch-index") && props.node.comment ? props.node.comment : null));
const paddingLeft = computed(() => treeItemPaddingLeft(props.depth));
const isConnected = computed(() => props.node.type === "connection" && !!props.node.connectionId && connectionStore.connectedIds.has(props.node.connectionId));
const isConnectionReadonly = computed(() => props.node.type === "connection" && !!props.node.connectionId && (connectionStore.getConfig(props.node.connectionId)?.read_only ?? false));
@ -2788,6 +2795,14 @@ function treeItemMenuItems(): ContextMenuItem[] {
return items;
}
if (node.type === "elasticsearch-index") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: toggle, icon: TableProperties });
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
return items;
}
// 6. Table / View
if (node.type === "table" || node.type === "view") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });

View File

@ -73,7 +73,7 @@ const ddlLoading = ref(false);
const ddlFetched = ref(false);
async function fetchDdl() {
if (!props.connectionId || !props.database || !props.tableName || ddlFetched.value) return;
if (!props.connectionId || !props.database || !props.tableName || ddlFetched.value || !tableMetadataCapabilities.value.ddl) return;
ddlLoading.value = true;
try {
ddlContent.value = await api.getTableDdl(props.connectionId, props.database, metadataSchema.value, props.tableName);
@ -373,7 +373,7 @@ async function loadStructure(silent = false) {
await store.ensureConnected(props.connectionId);
const nextColumns = await api.getColumns(props.connectionId, props.database, metadataSchema.value, props.tableName);
const [nextIndexes, nextForeignKeys, nextTriggers] = await Promise.all([
api.listIndexes(props.connectionId, props.database, metadataSchema.value, props.tableName).catch(() => []),
tableMetadataCapabilities.value.indexes ? api.listIndexes(props.connectionId, props.database, metadataSchema.value, props.tableName).catch(() => []) : Promise.resolve([]),
tableMetadataCapabilities.value.foreignKeys ? api.listForeignKeys(props.connectionId, props.database, metadataSchema.value, props.tableName).catch(() => []) : Promise.resolve([]),
tableMetadataCapabilities.value.triggers ? api.listTriggers(props.connectionId, props.database, metadataSchema.value, props.tableName).catch(() => []) : Promise.resolve([]),
]);
@ -672,9 +672,23 @@ onBeforeUnmount(() => {
unregisterStructureEditorShortcuts();
});
function firstStructureMetadataTab(capabilities = tableMetadataCapabilities.value) {
if (capabilities.columns) return "columns";
if (capabilities.indexes) return "indexes";
if (capabilities.foreignKeys) return "foreignKeys";
if (capabilities.triggers) return "triggers";
if (capabilities.ddl && !isCreateMode.value) return "ddl";
return "columns";
}
watch(tableMetadataCapabilities, (capabilities) => {
if (activeTab.value === "foreignKeys" && !capabilities.foreignKeys) activeTab.value = "columns";
if (activeTab.value === "triggers" && !capabilities.triggers) activeTab.value = "columns";
const supported =
(activeTab.value === "columns" && capabilities.columns) ||
(activeTab.value === "indexes" && capabilities.indexes) ||
(activeTab.value === "foreignKeys" && capabilities.foreignKeys) ||
(activeTab.value === "triggers" && capabilities.triggers) ||
(activeTab.value === "ddl" && capabilities.ddl && !isCreateMode.value);
if (!supported) activeTab.value = firstStructureMetadataTab(capabilities);
});
watch(
@ -735,11 +749,11 @@ watch(activeTab, (tab) => {
<Tabs v-model="activeTab" class="flex h-full min-h-0 flex-col">
<div class="flex shrink-0 items-center justify-between gap-2 border-b px-2 py-[var(--structure-header-py)]">
<TabsList>
<TabsTrigger value="columns">{{ t("structureEditor.columns") }}</TabsTrigger>
<TabsTrigger value="indexes">{{ t("structureEditor.indexes") }}</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.columns" value="columns">{{ t("structureEditor.columns") }}</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.indexes" value="indexes">{{ t("structureEditor.indexes") }}</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.foreignKeys" value="foreignKeys">{{ t("structureEditor.foreignKeys") }}</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.triggers" value="triggers">{{ t("structureEditor.triggers") }}</TabsTrigger>
<TabsTrigger value="ddl" v-if="!isCreateMode">DDL</TabsTrigger>
<TabsTrigger v-if="tableMetadataCapabilities.ddl && !isCreateMode" value="ddl">DDL</TabsTrigger>
</TabsList>
<div class="flex shrink-0 items-center gap-1.5">
<div class="flex items-center gap-1.5">
@ -766,7 +780,7 @@ watch(activeTab, (tab) => {
</div>
</div>
<TabsContent value="columns" class="m-0 min-h-0 flex-1 overflow-auto p-0">
<TabsContent v-if="tableMetadataCapabilities.columns" value="columns" class="m-0 min-h-0 flex-1 overflow-auto p-0">
<table class="border-separate border-spacing-0 text-[length:var(--structure-font-size)] leading-[var(--structure-line-height)]" :style="{ minWidth: visibleColWidths.reduce((a, w) => a + w, 0) + 'px' }">
<thead class="sticky top-0 z-10 bg-background">
<tr>
@ -984,7 +998,7 @@ watch(activeTab, (tab) => {
</table>
</TabsContent>
<TabsContent value="indexes" class="m-0 min-h-0 flex-1 overflow-auto p-0">
<TabsContent v-if="tableMetadataCapabilities.indexes" value="indexes" class="m-0 min-h-0 flex-1 overflow-auto p-0">
<table class="border-separate border-spacing-0 text-[length:var(--structure-font-size)] leading-[var(--structure-line-height)]" :style="{ minWidth: indexColWidths.reduce((a, w) => a + w, 0) + 'px' }">
<thead class="sticky top-0 z-10 bg-background">
<tr>
@ -1108,7 +1122,7 @@ watch(activeTab, (tab) => {
</div>
</TabsContent>
<TabsContent value="ddl" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]">
<TabsContent v-if="tableMetadataCapabilities.ddl" value="ddl" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]">
<div v-if="ddlLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("common.loading") }}

View File

@ -266,6 +266,9 @@ export const mongoUpdateDocuments = forward("mongoUpdateDocuments");
export const mongoDeleteDocument = forward("mongoDeleteDocument");
export const mongoDeleteDocuments = forward("mongoDeleteDocuments");
// Elasticsearch
export const elasticsearchListIndices = forward("elasticsearchListIndices");
// History
export const saveHistory = forward("saveHistory");
export const loadHistory = forward("loadHistory");

View File

@ -1251,6 +1251,10 @@ export async function mongoListCollections(connectionId: string, database: strin
return post("/api/mongo/list-collections", { connectionId, database });
}
export async function elasticsearchListIndices(connectionId: string): Promise<string[]> {
return mongoListCollections(connectionId, "default");
}
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string): Promise<MongoDocumentResult> {
return post("/api/mongo/find-documents", { connectionId, database, collection, skip, limit, filter, sort });
}

View File

@ -84,6 +84,9 @@ function schemaMatches(node: TreeNode, schema: string | undefined): boolean {
export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): boolean {
if (target.type === "mongo-collection") {
if (node.type === "elasticsearch-index") {
return node.connectionId === target.connectionId && node.label === target.collectionName;
}
return node.type === "mongo-collection" && node.connectionId === target.connectionId && node.database === target.database && node.label === target.collectionName;
}

View File

@ -1,8 +1,8 @@
import type { TreeNodeType } from "@/types/database";
const leafTypes: Set<TreeNodeType> = new Set(["column", "index", "fkey", "trigger", "procedure", "function", "package", "package-body", "object-browser", "redis-db", "mongo-collection", "user-admin", "saved-sql-file"]);
const leafTypes: Set<TreeNodeType> = new Set(["column", "index", "fkey", "trigger", "procedure", "function", "package", "package-body", "object-browser", "redis-db", "mongo-collection", "elasticsearch-index", "user-admin", "saved-sql-file"]);
const fullWidthLabelTypes: Set<TreeNodeType> = new Set(["table", "view", "mongo-collection"]);
const fullWidthLabelTypes: Set<TreeNodeType> = new Set(["table", "view", "mongo-collection", "elasticsearch-index"]);
const emptyContainerTypes: Set<TreeNodeType> = new Set(["saved-sql-root", "saved-sql-folder"]);

View File

@ -21,6 +21,12 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
foreignKeys: false,
triggers: false,
},
elasticsearch: {
indexes: false,
foreignKeys: false,
triggers: false,
ddl: false,
},
influxdb: {
indexes: false,
foreignKeys: false,

View File

@ -1150,6 +1150,10 @@ export async function mongoListCollections(connectionId: string, database: strin
return invoke("mongo_list_collections", { connectionId, database });
}
export async function elasticsearchListIndices(connectionId: string): Promise<string[]> {
return mongoListCollections(connectionId, "default");
}
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, sort?: string): Promise<MongoDocumentResult> {
return invoke("mongo_find_documents", { connectionId, database, collection, skip, limit, filter, sort });
}

View File

@ -7,7 +7,7 @@ export type SidebarSelectionCopyAction = "copy-name" | "none";
export type SidebarActivation = "single" | "double";
const dataNodeTypes = new Set<TreeNodeType>(["table", "view"]);
const toggleLeafNodeTypes = new Set<TreeNodeType>(["redis-db", "mongo-collection", "user-admin"]);
const toggleLeafNodeTypes = new Set<TreeNodeType>(["redis-db", "mongo-collection", "elasticsearch-index", "user-admin"]);
const objectBrowserNodeTypes = new Set<TreeNodeType>(["database", "schema", "object-browser"]);
const sourceNodeTypes = new Set<TreeNodeType>(["procedure", "function", "sequence", "package", "package-body"]);
const savedSqlNodeTypes = new Set<TreeNodeType>(["saved-sql-file"]);

View File

@ -522,16 +522,16 @@ export const useConnectionStore = defineStore("connection", () => {
for (const key of Object.keys(elasticsearchCompletionIndicesCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete elasticsearchCompletionIndicesCache.value[key];
}
for (const key of [...completionTableIndex.keys()]) {
for (const key of completionTableIndex.keys()) {
if (key.startsWith(cachePrefix)) completionTableIndex.delete(key);
}
for (const key of [...completionObjectIndex.keys()]) {
for (const key of completionObjectIndex.keys()) {
if (key.startsWith(cachePrefix)) completionObjectIndex.delete(key);
}
for (const key of [...completionColumnIndex.keys()]) {
for (const key of completionColumnIndex.keys()) {
if (key.startsWith(cachePrefix)) completionColumnIndex.delete(key);
}
for (const key of [...completionInFlight.keys()]) {
for (const key of completionInFlight.keys()) {
if (key.startsWith(cachePrefix)) completionInFlight.delete(key);
}
}
@ -645,6 +645,8 @@ export const useConnectionStore = defineStore("connection", () => {
await loadEtcdRoot(connectionId);
} else if (config.db_type === "mongodb") {
await loadMongoDatabases(connectionId);
} else if (config.db_type === "elasticsearch") {
await loadElasticsearchIndices(connectionId);
} else {
await loadDatabases(connectionId, { force: true });
}
@ -975,6 +977,38 @@ export const useConnectionStore = defineStore("connection", () => {
}
}
async function loadElasticsearchIndices(connectionId: string) {
const node = findNode(treeNodes.value, connectionId);
if (!node) return;
node.isLoading = true;
try {
await ensureConnected(connectionId);
const indices = await api.elasticsearchListIndices(connectionId);
setChildren(
node,
withSavedSqlRoot(
connectionId,
sortSidebarNames(indices).map((index) => ({
id: `${connectionId}:__es_index:${index}`,
label: index,
type: "elasticsearch-index" as const,
connectionId,
database: "default",
isExpanded: false,
})),
node,
),
);
node.isExpanded = true;
} catch (e) {
recordMetadataLoadError(connectionId, e);
throw e;
} finally {
node.isLoading = false;
}
}
async function loadMongoCollections(connectionId: string, database: string) {
const nodeId = `${connectionId}:${database}`;
const node = findNode(treeNodes.value, nodeId);
@ -1408,8 +1442,10 @@ export const useConnectionStore = defineStore("connection", () => {
await loadRedisDatabases(node.connectionId);
} else if (config?.db_type === "etcd") {
await loadEtcdRoot(node.connectionId);
} else if (config?.db_type === "mongodb" || config?.db_type === "elasticsearch") {
} else if (config?.db_type === "mongodb") {
await loadMongoDatabases(node.connectionId);
} else if (config?.db_type === "elasticsearch") {
await loadElasticsearchIndices(node.connectionId);
} else {
await loadDatabases(node.connectionId, options);
}
@ -1700,7 +1736,7 @@ export const useConnectionStore = defineStore("connection", () => {
return elasticsearchCompletionIndicesCache.value[cacheKey];
}
await ensureConnected(connectionId);
const indices = await api.mongoListCollections(connectionId, database);
const indices = await api.elasticsearchListIndices(connectionId);
elasticsearchCompletionIndicesCache.value[cacheKey] = indices;
evictOldestCacheEntries(elasticsearchCompletionIndicesCache.value, COMPLETION_CACHE_MAX);
return elasticsearchCompletionIndicesCache.value[cacheKey];
@ -2435,6 +2471,7 @@ export const useConnectionStore = defineStore("connection", () => {
loadEtcdRoot,
updateRedisDbKeyStats,
loadMongoDatabases,
loadElasticsearchIndices,
loadMongoCollections,
loadSchemas,
loadSqlServerDatabaseObjects,

View File

@ -325,7 +325,8 @@ export type TreeNodeType =
| "redis-db"
| "etcd-root"
| "mongo-db"
| "mongo-collection";
| "mongo-collection"
| "elasticsearch-index";
export interface ConnectionGroup {
id: string;