diff --git a/apps/desktop/public/icons/database/milvus.png b/apps/desktop/public/icons/database/milvus.png new file mode 100644 index 0000000000..4d3214c7df Binary files /dev/null and b/apps/desktop/public/icons/database/milvus.png differ diff --git a/apps/desktop/public/icons/database/qdrant.svg b/apps/desktop/public/icons/database/qdrant.svg new file mode 100644 index 0000000000..c10ad96f11 --- /dev/null +++ b/apps/desktop/public/icons/database/qdrant.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index b877e89b2c..fe5615f4c0 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -407,6 +407,8 @@ const driverProfiles: Record< label: "Elasticsearch", icon: "elasticsearch", }, + qdrant: { type: "qdrant", port: 6333, user: "", label: "Qdrant", icon: "qdrant" }, + milvus: { type: "milvus", port: 19530, user: "root", label: "Milvus", icon: "milvus" }, mariadb: { type: "mysql", port: 3306, user: "root", label: "MariaDB", icon: "mariadb" }, tidb: { type: "mysql", port: 4000, user: "root", label: "TiDB", icon: "tidb" }, oceanbase: { type: "mysql", port: 2881, user: "root", label: "OceanBase", icon: "oceanbase" }, @@ -870,6 +872,8 @@ const iconTypeMap: Record = { sqlserver: "sqlserver", oracle: "oracle", elasticsearch: "elasticsearch", + qdrant: "qdrant", + milvus: "milvus", mariadb: "mariadb", tidb: "tidb", oceanbase: "oceanbase", @@ -933,6 +937,8 @@ const dbOptions: DbOption[] = [ { value: "sqlite", label: "SQLite" }, { value: "sqlserver", label: "SQL Server" }, { value: "elasticsearch", label: "Elasticsearch" }, + { value: "qdrant", label: "Qdrant" }, + { value: "milvus", label: "Milvus" }, { value: "dm", label: "DM (Dameng)" }, { value: "opengauss", label: "openGauss" }, { value: "turso", label: "Turso" }, @@ -1034,7 +1040,7 @@ const sqliteExtensionPaths = computed({ form.value.url_params = setSqliteExtensionPaths(form.value.url_params, value); }, }); -const tlsCapableDatabaseTypes = new Set(["mysql", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "redis", "etcd", "clickhouse", "elasticsearch", "influxdb"]); +const tlsCapableDatabaseTypes = new Set(["mysql", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "redis", "etcd", "clickhouse", "elasticsearch", "qdrant", "milvus", "influxdb"]); const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type)); const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse"); const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch"); diff --git a/apps/desktop/src/components/diff/DataCompareDialog.vue b/apps/desktop/src/components/diff/DataCompareDialog.vue index 7ad992c20b..9117cb0cd2 100644 --- a/apps/desktop/src/components/diff/DataCompareDialog.vue +++ b/apps/desktop/src/components/diff/DataCompareDialog.vue @@ -114,7 +114,7 @@ const showModified = ref(true); let syncPlanRequestId = 0; -const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "etcd"].includes(connection.db_type))); +const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd"].includes(connection.db_type))); const selectedSourceTableNames = computed(() => sourceTables.value.filter((table) => selectedSourceTables.value.has(table))); const isBatchCompare = computed(() => selectedSourceTableNames.value.length > 1); const filteredSourceTables = computed(() => { diff --git a/apps/desktop/src/components/export/DatabaseExportDialog.vue b/apps/desktop/src/components/export/DatabaseExportDialog.vue index acc59c5706..fc7f73fb91 100644 --- a/apps/desktop/src/components/export/DatabaseExportDialog.vue +++ b/apps/desktop/src/components/export/DatabaseExportDialog.vue @@ -65,7 +65,7 @@ const exportCancelled = ref(false); const pendingPrefillTable = ref(""); const pendingPrefillTables = ref([]); -const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "etcd"].includes(c.db_type))); +const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd"].includes(c.db_type))); const canExport = computed(() => connectionId.value && database.value && schema.value && !loadingTables.value && !tableError.value && (tables.value.length === 0 || selectedTables.value.length > 0) && (includeStructure.value || includeData.value || includeObjects.value) && !isExporting.value); diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue index ec1bb00b04..82565275d9 100644 --- a/apps/desktop/src/components/icons/DatabaseIcon.vue +++ b/apps/desktop/src/components/icons/DatabaseIcon.vue @@ -76,6 +76,8 @@ const assetIcons: Record = { xugu: "xugu.png", iotdb: "iotdb", etcd: "etcd", + qdrant: "qdrant", + milvus: "milvus.png", mq: "pulsar", pulsar: "pulsar", iris: "iris.png", diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue index 21837b5f97..b27605289f 100644 --- a/apps/desktop/src/components/layout/AppTabBar.vue +++ b/apps/desktop/src/components/layout/AppTabBar.vue @@ -216,7 +216,7 @@ function tabColorStyle(tab: QueryTab) { } function tabIconClass(tab: QueryTab) { - if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "redis" || tab.mode === "objects" || tab.mode === "structure") return "text-emerald-600 dark:text-emerald-400"; + if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "vector" || tab.mode === "redis" || tab.mode === "objects" || tab.mode === "structure") return "text-emerald-600 dark:text-emerald-400"; return "text-blue-600 dark:text-blue-400"; } @@ -234,6 +234,7 @@ const openTabMenuItems = computed(() => function tabMenuIcon(tab: QueryTab) { if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "redis") return Table2; + if (tab.mode === "vector") return TableProperties; if (tab.mode === "etcd") return KeyRound; if (tab.mode === "objects") return TableProperties; if (tab.mode === "structure") return PencilRuler; @@ -326,6 +327,7 @@ function activateTab(tabId: string) { > + diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index a6981f161d..0e6474e97d 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -36,6 +36,7 @@ const DataGrid = defineAsyncComponent(loadDataGridComponent); const RedisKeyBrowser = defineAsyncComponent(() => import("@/components/redis/RedisKeyBrowser.vue")); const EtcdKeyBrowser = defineAsyncComponent(() => import("@/components/etcd/EtcdKeyBrowser.vue")); const DocumentBrowser = defineAsyncComponent(() => import("@/components/document/DocumentBrowser.vue")); +const VectorBrowser = defineAsyncComponent(() => import("@/components/vector/VectorBrowser.vue")); const MqAdminConsole = defineAsyncComponent(() => import("@/components/mq/MqAdminConsole.vue")); const ObjectBrowser = defineAsyncComponent(() => import("@/components/objects/ObjectBrowser.vue")); const TableStructureEditor = defineAsyncComponent(() => import("@/components/structure/TableStructureEditor.vue")); @@ -993,6 +994,13 @@ defineExpose({ focusSearch, refreshData, handleModRTarget }); + + + -
+
= { connection: ["connection"], database: ["database", "redis-db", "mq-tenant", "mongo-db"], schema: ["schema"], - table: ["table", "mongo-collection", "elasticsearch-index"], + table: ["table", "mongo-collection", "vector-collection", "elasticsearch-index"], view: ["view"], }; @@ -336,6 +336,8 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?: await store.loadMongoDatabases(connId); } else if (config.db_type === "elasticsearch") { await store.loadElasticsearchIndices(connId); + } else if (config.db_type === "qdrant" || config.db_type === "milvus") { + await store.loadVectorCollections(connId); } else if (config.db_type === "mq") { await store.loadMqTenants(connId, loadOptions); } else { diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 45964cc876..dc7433e8a7 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -247,6 +247,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 "vector-collection": + return { icon: TableProperties, colorClass: "text-cyan-400" }; case "elasticsearch-index": return { icon: Table, colorClass: "text-emerald-400" }; case "procedure": @@ -283,7 +285,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-materialized-views", "group-procedures", "group-functions", "group-sequences", "group-packages", "group-partitions"]); -const pinnableTypes: Set = new Set(["connection-group", "database", "linked-server", "linked-server-catalog", "linked-server-schema", "schema", "table", "view", "materialized_view", "redis-db", "mongo-db", "mongo-collection", "elasticsearch-index"]); +const pinnableTypes: Set = new Set(["connection-group", "database", "linked-server", "linked-server-catalog", "linked-server-schema", "schema", "table", "view", "materialized_view", "redis-db", "mongo-db", "mongo-collection", "vector-collection", "elasticsearch-index"]); function isGroupLabel(node: TreeNode): boolean { return groupTypes.has(node.type); @@ -299,7 +301,7 @@ function displayLabel(node: TreeNode): string { } function visibleLabel(node: TreeNode): string { - if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "elasticsearch-index") { + if (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "vector-collection" || node.type === "elasticsearch-index") { return sidebarDisplayTableName(node.label, settingsStore.editorSettings.sidebarHiddenTablePrefixes); } return displayLabel(node); @@ -335,6 +337,8 @@ function connectionTooltipScheme(config: Pick { 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(() => (!settingsStore.editorSettings.sidebarHideTableComments && props.node.type === "column" && props.node.meta && "comment" in props.node.meta ? (props.node.meta as any).comment : null)); -const tableComment = computed(() => (!settingsStore.editorSettings.sidebarHideTableComments && (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 tableComment = computed(() => + !settingsStore.editorSettings.sidebarHideTableComments && (props.node.type === "table" || props.node.type === "view" || props.node.type === "mongo-collection" || props.node.type === "vector-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)); @@ -2735,7 +2746,7 @@ const nodeIconClass = computed(() => { const canConfigureVisibleDatabases = computed(() => { if (props.node.type !== "connection" || !props.node.connectionId) return false; const dbType = connectionStore.getConfig(props.node.connectionId)?.db_type; - return dbType !== "elasticsearch" && dbType !== "etcd" && dbType !== "mq"; + return dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "etcd" && dbType !== "mq"; }); const canCopyFinalProxyPort = computed(() => { if (props.node.type !== "connection" || !props.node.connectionId) return false; @@ -3325,7 +3336,7 @@ function treeItemMenuItems(): ContextMenuItem[] { return items; } - if (node.type === "elasticsearch-index") { + if (node.type === "elasticsearch-index" || node.type === "vector-collection") { 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 }); diff --git a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue index f60e1d0181..a515027d50 100644 --- a/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue +++ b/apps/desktop/src/components/sql-file/SqlFileExecutionDialog.vue @@ -49,7 +49,7 @@ const terminalStatus = ref("idle"); const terminalError = ref(""); const refreshedTarget = ref(false); -const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "etcd"].includes(c.db_type))); +const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "etcd"].includes(c.db_type))); const selectedConnection = computed(() => sqlConnections.value.find((c) => c.id === connectionId.value)); diff --git a/apps/desktop/src/components/vector/VectorBrowser.vue b/apps/desktop/src/components/vector/VectorBrowser.vue new file mode 100644 index 0000000000..8a8a6daa81 --- /dev/null +++ b/apps/desktop/src/components/vector/VectorBrowser.vue @@ -0,0 +1,256 @@ + + +