feat(elasticsearch): model indices as first-class nodes
This commit is contained in:
1 parent
6f253abe55
commit
26d714909e
15 files changed
+146
-38
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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") }}
|
||||
|
||||
Reference in new issue
Block a user