diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue
index 887520c85..2e09dca6a 100644
--- a/apps/desktop/src/components/grid/DataGrid.vue
+++ b/apps/desktop/src/components/grid/DataGrid.vue
@@ -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();
diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue
index 114ada640..47d4e08af 100644
--- a/apps/desktop/src/components/layout/ContentArea.vue
+++ b/apps/desktop/src/components/layout/ContentArea.vue
@@ -820,10 +820,10 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
-
+
-
+
diff --git a/apps/desktop/src/components/mongo/MongoDocBrowser.vue b/apps/desktop/src/components/mongo/MongoDocBrowser.vue
index a89908af2..7b105cb1c 100644
--- a/apps/desktop/src/components/mongo/MongoDocBrowser.vue
+++ b/apps/desktop/src/components/mongo/MongoDocBrowser.vue
@@ -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;
@@ -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() {
- {{ t("mongo.documents", { count: total }) }}
+ {{ documentStoreLabels.documentsLabel }}
@@ -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"
diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue
index 8e8965da8..67bb8e1e3 100644
--- a/apps/desktop/src/components/sidebar/ConnectionTree.vue
+++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue
@@ -57,7 +57,7 @@ const SEARCH_SCOPE_TO_NODE_TYPES: Record = {
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);
}
diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index 94e2fd8bd..a8f93f231 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -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 = 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 = new Set(["connection-group", "database", "schema", "table", "view", "redis-db", "mongo-db", "mongo-collection"]);
+const pinnableTypes: Set = 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 });
diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue
index 48f836442..34d0092fe 100644
--- a/apps/desktop/src/components/structure/TableStructureEditor.vue
+++ b/apps/desktop/src/components/structure/TableStructureEditor.vue
@@ -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) => {