6876 lines
289 KiB
Vue
6876 lines
289 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, watch, onBeforeUnmount, inject, reactive, ref, shallowRef } from "vue";
|
|
import { createRoutedSidebarDialogController } from "./sidebarDialogControllerRouting";
|
|
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
|
import { useSidebarDataOpenRuntime } from "@/composables/useSidebarDataOpenRuntime";
|
|
import { useSidebarConnectionMutationRuntime } from "@/composables/useSidebarConnectionMutationRuntime";
|
|
import { useSidebarDatabaseSpecificMutationRuntime } from "@/composables/useSidebarDatabaseSpecificMutationRuntime";
|
|
import { useSidebarTableMutationRuntime } from "@/composables/useSidebarTableMutationRuntime";
|
|
import { useSidebarTreeExportRuntime } from "@/composables/useSidebarTreeExportRuntime";
|
|
import { useSidebarTreeToolRuntime } from "@/composables/useSidebarTreeToolRuntime";
|
|
import { useI18n } from "vue-i18n";
|
|
import { translateBackendError } from "@/i18n/backend-errors";
|
|
import {
|
|
BarChart3,
|
|
BookOpen,
|
|
Braces,
|
|
Database,
|
|
ChevronsDown,
|
|
FolderOpen,
|
|
Trash2,
|
|
TerminalSquare,
|
|
RefreshCw,
|
|
Copy,
|
|
TableProperties,
|
|
ListTree,
|
|
Pencil,
|
|
Play,
|
|
Plug,
|
|
Unplug,
|
|
Pin,
|
|
PlugZap,
|
|
ArrowRightLeft,
|
|
Download,
|
|
Eye,
|
|
Upload,
|
|
FileCode,
|
|
Network,
|
|
PencilRuler,
|
|
Search,
|
|
FolderInput,
|
|
FolderPlus,
|
|
Eraser,
|
|
Scissors,
|
|
CopyPlus,
|
|
Plus,
|
|
ScrollText,
|
|
Code2,
|
|
Wrench,
|
|
ListFilter,
|
|
Clipboard,
|
|
UsersRound,
|
|
ShieldCheck,
|
|
Activity,
|
|
Gauge,
|
|
CalendarClock,
|
|
HardDriveDownload,
|
|
FilePlus,
|
|
SquarePen,
|
|
ListX,
|
|
Info,
|
|
X,
|
|
Settings2,
|
|
GitBranch,
|
|
Sparkles,
|
|
Link2,
|
|
} from "@lucide/vue";
|
|
import type { ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
|
import { CONNECTION_ATTEMPT_CANCELLED_MESSAGE, useConnectionStore } from "@/stores/connectionStore";
|
|
import { useQueryStore } from "@/stores/queryStore";
|
|
import { useSettingsStore } from "@/stores/settingsStore";
|
|
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
|
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
|
|
import { useToast } from "@/composables/useToast";
|
|
import { createFrontendPluginRegistry } from "@/lib/plugins/frontendPlugin";
|
|
import type { InstalledPlugin } from "@/types/database";
|
|
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
|
|
import type { ColumnInfo, ConnectionConfig, DatabaseType, TreeNode, TreeNodeType } from "@/types/database";
|
|
import * as api from "@/lib/backend/api";
|
|
import type { ElasticsearchIndexMetadataKind } from "@/lib/backend/tauri";
|
|
import { queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
|
|
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
|
import { connectionUsesVisibleSchemaFilter } from "@/lib/database/visibleDatabases";
|
|
import { canTreeNodePin, canTreeNodeShowExpander } from "@/lib/sidebar/sidebarTreeItemLayout";
|
|
import { sidebarConnectionVisibleFilterMenu } from "@/lib/sidebar/sidebarVisibleFilterMenu";
|
|
import { supportsSidebarObjectNameFilter } from "@/lib/sidebar/sidebarObjectNameFilter";
|
|
import { connectionGroupDestinationRows } from "@/lib/sidebar/sidebarLayout";
|
|
import { hasTableVGroupEntries, selectedTableVGroupMoveTargets, tableVGroupDestinationRows, tableVGroupPathForTable, tableVGroupsEnabled } from "@/lib/table/tableVGroup";
|
|
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
|
|
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
|
|
import { requestObjectBrowserSearchFocus } from "@/lib/tabs/objectBrowserSearchFocus";
|
|
import { isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
|
|
import { isXuguSyntheticTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
|
|
import { buildXuguSchedulerJobSql, type XuguSchedulerJobAction } from "@/lib/database/xuguSchedulerJobSql";
|
|
import { canViewDatabaseObjectDependencies, databaseDependencyProviderFor } from "@/lib/database/databaseObjectDependencies";
|
|
import { elasticsearchClearIndexPreview, isElasticsearchClearConfirmed, isElasticsearchIndexPattern } from "@/lib/sidebar/elasticsearchIndexActions";
|
|
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
|
|
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
|
|
import { joinExportedDdls } from "@/lib/export/ddlExport";
|
|
import { qualifiedTableName } from "@/lib/table/tableSelectSql";
|
|
import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint";
|
|
import {
|
|
canCreateConnectionNamespace,
|
|
canCreateDatabaseNodeNamespace,
|
|
canEditDatabaseProperties as canEditDatabasePropertiesForNode,
|
|
connectionNamespaceCreationTarget,
|
|
editableDatabasePropertyGroups,
|
|
supportsDatabaseCreation,
|
|
supportsDatabaseSearch,
|
|
supportsConnectionDatabaseBrowser,
|
|
supportsConnectionQueryActions,
|
|
supportsAiAssistantContext,
|
|
supportsFieldLineage,
|
|
supportsObjectBrowserTreeNode,
|
|
supportsSchemaDiagram,
|
|
supportsSqlFileExecution,
|
|
supportsTableImport,
|
|
supportsTableTruncate,
|
|
supportsTableStructureEditing,
|
|
supportsTransfer,
|
|
supportsPackageMemberExpansion,
|
|
usesTreeSchemaMode,
|
|
isSingleDatabase,
|
|
schemaNodeHasLoadableName,
|
|
} from "@/lib/database/databaseCapabilities";
|
|
import { copyDisplayPathForTreeNode, copyNameForTreeNode, isDirectNavigationTreeNode, isDocumentBrowserTreeNode, isRepeatableNavigationTreeNode, objectSourceTargetForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/sidebar/treeNodeClick";
|
|
import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
|
|
import { mongoCollectionTableTypeFromNode, mongoCreateDatabasePreview, mongoDropIndexFailureCount } from "@/lib/sidebar/mongoCollectionMutation";
|
|
import { dataTabOpenModeFromTreeClick, type DataTabOpenMode } from "@/lib/sidebar/dataTabOpenPolicy";
|
|
import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isModRShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts";
|
|
import { handleSidebarTreeDeleteShortcut } from "@/lib/sidebar/sidebarTreeDeleteShortcut";
|
|
import { dataTableDoubleClickAction } from "@/lib/tabs/dataTabActivation";
|
|
import { attachedDatabaseNameFromPath, buildCreateDatabaseSql, buildDuckDbAttachDatabaseSql, buildSqliteAttachDatabaseSql, supportsCreateDatabaseCharset, supportsCreateDatabaseLocale, uniqueAttachedDatabaseName } from "@/lib/database/createDatabaseSql";
|
|
import { appendCreateDatabaseErrorHint } from "@/lib/database/createDatabaseErrorHints";
|
|
import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection";
|
|
import {
|
|
buildCreateSchemaSql,
|
|
buildDropDatabaseSql,
|
|
buildDropObjectSql,
|
|
buildDropSchemaSql,
|
|
damengDropSchemaExecutionSchema,
|
|
buildGetDatabaseCommentSql,
|
|
buildGetSchemaCommentSql,
|
|
buildUpdateDatabasePropertiesSql,
|
|
buildDropTableSql,
|
|
buildDropTableChildObjectSql,
|
|
buildDuplicateTableStructurePlan,
|
|
buildCopyTableDataSql,
|
|
buildEmptyTableSql,
|
|
buildTruncateTableSql,
|
|
buildMysqlAutoIncrementSql,
|
|
supportsDropTableCascade,
|
|
supportsTruncateTableCascade,
|
|
supportsNativeMysqlAutoIncrement,
|
|
supportsSchemaComment,
|
|
type DropTableChildObjectSqlOptions,
|
|
type DropObjectSqlOptions,
|
|
type MysqlAutoIncrementSqlOptions,
|
|
type TableChildObjectType,
|
|
} from "@/lib/database/dbAdminSql";
|
|
import { buildRenameObjectSql, buildRenameDatabaseSql, buildRenameDatabasePreflightSql, databaseRenameMaintenanceDatabase, supportsDatabaseRename, supportsObjectRename, type RenameableObjectType } from "@/lib/table/objectRenameSql";
|
|
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
|
|
import { buildViewDdl } from "@/lib/table/viewDdl";
|
|
import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
|
|
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
|
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionTableSqlSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
|
import { isObjectCacheInvalidationError } from "@/lib/metadata/objectCacheInvalidationError";
|
|
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
|
|
import {
|
|
defaultPasteTableMode,
|
|
pasteTableModeCopiesData,
|
|
supportsWholeRowTableDataCopy,
|
|
tableClipboardMatchesTarget,
|
|
tableClipboardMenuState,
|
|
tableClipboardSourceContext,
|
|
tableDataCopyColumnOptions,
|
|
tablePasteFeedback,
|
|
type TableClipboardContext,
|
|
type TableClipboardTableContext,
|
|
} from "@/lib/table/tableClipboard";
|
|
import { selectedSidebarBatchTargets, selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes } from "@/lib/sidebar/sidebarTreeSelection";
|
|
import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionEditTarget, selectedConnectionMoveTargets } from "@/lib/sidebar/sidebarConnectionSelection";
|
|
import { connectionSupportsDatabaseUserAdmin, resolveDatabaseUserAdminProviderForConnection, type DatabaseUserIdentity } from "@/lib/database/databaseUserAdmin";
|
|
import { authorizationPlanSql, authorizationPlanStatus, buildCreateDatabaseAuthorizationPlan, executeAuthorizationPlan, type AuthorizationPlan, type AuthorizationStepResult } from "@/lib/database/databaseAuthorizationPlan";
|
|
import { connectionSupportsProcessList } from "@/lib/database/processListDrivers";
|
|
import { connectionSupportsServerDashboard } from "@/lib/database/mysqlServerStatus";
|
|
import { connectionSupportsServerDashboard as connectionSupportsPgServerDashboard } from "@/lib/database/postgresServerStatus";
|
|
import { connectionSupportsXuguServerDashboard } from "@/lib/database/xuguServerStatus";
|
|
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
|
import { sidebarTreeArrowAction } from "@/lib/sidebar/sidebarTreeArrowNavigation";
|
|
import { batchTableEmptyFeedback, runBatchTableEmpty } from "@/lib/sidebar/batchTableEmpty";
|
|
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
|
|
import { runBatchTableDrop } from "@/lib/table/batchTableDrop";
|
|
import { buildSidebarDdlTemplateSql, formatSidebarDdlTemplateForDisplay } from "@/lib/sidebar/sidebarDdlTemplate";
|
|
import { resolveSidebarDdlTargets } from "@/lib/sidebar/sidebarDdlTargets";
|
|
import { sidebarTableDataExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
|
import { formatSidebarTableCopyText, type FormatSidebarTableNamesOptions } from "@/lib/sidebar/sidebarTableNameCopy";
|
|
import { supportsScheduledDatabaseBackup } from "@/lib/backup/scheduledDatabaseBackup";
|
|
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
|
import { copyToClipboard } from "@/lib/common/clipboard";
|
|
import { buildConnectionUrlCopy, CONNECTION_URL_COPY_WITH_PASSWORD_FORMATS, connectionUrlCopyFormats, type ConnectionUrlCopyFormat } from "@/lib/connection/connectionUrlBuilder";
|
|
import { rankSavedSqlHistory, type SavedSqlHistoryScope } from "@/lib/savedSql/savedSqlHistory";
|
|
import { savedSqlClipboardFileIds, savedSqlPasteTargetForNode } from "@/lib/savedSql/savedSqlClipboard";
|
|
import { exportSavedSqlFileContent } from "@/lib/savedSql/savedSqlExport";
|
|
import { isSqlServerLinkedNode } from "@/lib/database/sqlServerLinkedServers";
|
|
import { flattenTree } from "@/composables/useFlatTree";
|
|
import { createDatabaseCollationOptionsForCharset, DEFAULT_GBASE8S_DATABASE_LOCALE, defaultGbase8sDatabaseLocale, GBASE8S_DATABASE_LOCALES, nextCreateDatabaseCollation, normalizeCreateDatabaseCharset, parseCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
|
import { executeWithProductionContextGuard, executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
|
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
|
|
import { buildXuguCompileSql } from "@/lib/database/xuguCompileSql";
|
|
import { buildDamengCompileViewSql } from "@/lib/database/damengCompileSql";
|
|
import type { SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
|
|
import { createSidebarActionTarget, findSidebarActionTarget, releaseRemovedSidebarActionTarget, type SidebarActionTarget } from "@/lib/sidebar/sidebarActionTarget";
|
|
import { createSidebarMenuContext, normalizeSidebarMenuDescriptors } from "@/lib/sidebar/sidebarTreeMenuDescriptors";
|
|
import { driverProfileDatabaseWorkspace } from "@/lib/database/driverProfileExtensions";
|
|
import type { SidebarDangerDialogRequest } from "@/lib/sidebar/sidebarDangerDialog";
|
|
import {
|
|
fallbackCreateDatabaseCharset,
|
|
sidebarTreeDialogOwner,
|
|
sidebarDangerTarget,
|
|
sidebarDangerRunningCancel,
|
|
sidebarFormTarget,
|
|
showDeleteConfirm,
|
|
showTableVGroupDialog,
|
|
showTableVGroupDeleteConfirm,
|
|
tableVGroupDeleteTarget,
|
|
tableVGroupName,
|
|
tableVGroupDialogScope,
|
|
tableVGroupDialogParentGroupId,
|
|
tableVGroupDialogTableNames,
|
|
showDropTableConfirm,
|
|
showDropTableChildObjectConfirm,
|
|
showBatchDropConfirm,
|
|
showBatchEmptyConfirm,
|
|
showBatchTruncateConfirm,
|
|
showStructurePreviewDialog,
|
|
showStructureDocCopyDialog,
|
|
structurePreviewSql,
|
|
structurePreviewTitle,
|
|
structurePreviewError,
|
|
structureDocCopyText,
|
|
structureDocCopyTitle,
|
|
isLoadingStructurePreview,
|
|
showEmptyTableConfirm,
|
|
showTruncateTableConfirm,
|
|
showVacuumTableConfirm,
|
|
showMysqlAutoIncrementConfirm,
|
|
showBatchMysqlAutoIncrementConfirm,
|
|
batchMysqlAutoIncrementTargets,
|
|
batchMysqlAutoIncrementPreviewSql,
|
|
showRenameObjectDialog,
|
|
renameObjectName,
|
|
renameObjectError,
|
|
renameObjectPreviewSql,
|
|
dropTablePreviewSql,
|
|
dropTableCascade,
|
|
batchDropCascade,
|
|
emptyTablePreviewSql,
|
|
truncateTablePreviewSql,
|
|
truncateTableCascade,
|
|
vacuumTableFull,
|
|
vacuumTableAnalyze,
|
|
vacuumTablePreviewSql,
|
|
vacuumTableExecuting,
|
|
mysqlAutoIncrementValue,
|
|
mysqlAutoIncrementPreviewSql,
|
|
dropObjectPreviewSql,
|
|
showDropObjectConfirm,
|
|
dropTableChildObjectPreviewSql,
|
|
batchDropPreviewSql,
|
|
batchEmptyPreviewSql,
|
|
batchEmptyTargets,
|
|
batchDropTargets,
|
|
batchTruncateTargets,
|
|
batchTruncatePreviewSql,
|
|
batchTruncateCascade,
|
|
dropDatabasePreviewSql,
|
|
dropSchemaPreviewSql,
|
|
showDuplicateDialog,
|
|
duplicateTableName,
|
|
duplicateStructureSource,
|
|
showPasteDialog,
|
|
pasteTableMode,
|
|
pasteTableEntries,
|
|
showCreateDatabaseDialog,
|
|
createDatabaseName,
|
|
createDatabaseCharset,
|
|
createDatabaseCollation,
|
|
createDatabaseUsers,
|
|
createDatabaseSelectedUsers,
|
|
createDatabaseUsersLoading,
|
|
showCreateDatabasePreviewDialog,
|
|
createDatabaseAuthorizationPlan,
|
|
createDatabasePreviewSql,
|
|
createDatabaseAuthorizationResults,
|
|
createDatabaseAuthorizationApplying,
|
|
showCreateNacosNamespaceDialog,
|
|
createNacosNamespaceId,
|
|
createNacosNamespaceName,
|
|
createNacosNamespaceDesc,
|
|
createNacosNamespaceLoading,
|
|
showEditNacosNamespaceDialog,
|
|
editNacosNamespaceName,
|
|
editNacosNamespaceDesc,
|
|
editNacosNamespaceLoading,
|
|
showDeleteNacosNamespaceConfirm,
|
|
deleteNacosNamespaceLoading,
|
|
createDatabaseCharsetOptions,
|
|
createDatabaseCollationsByCharset,
|
|
createDatabaseCharsetLoading,
|
|
showDropDatabaseConfirm,
|
|
dropDatabaseLoading,
|
|
showDropMongoCollectionConfirm,
|
|
dropMongoCollectionLoading,
|
|
showDropMongoIndexConfirm,
|
|
dropMongoIndexLoading,
|
|
showDropAllMongoIndexesConfirm,
|
|
dropAllMongoIndexesLoading,
|
|
showCreateMongoIndexDialog,
|
|
showCreateMeilisearchIndexDialog,
|
|
meilisearchCreateIndexUid,
|
|
meilisearchCreateIndexPrimaryKey,
|
|
meilisearchCreateIndexError,
|
|
meilisearchCreateIndexLoading,
|
|
mongoCreateIndexForm,
|
|
mongoCreateIndexFieldOptions,
|
|
mongoCreateIndexError,
|
|
mongoCreateIndexLoading,
|
|
showMongoIndexManagerDialog,
|
|
mongoIndexManagerRows,
|
|
mongoIndexManagerLoading,
|
|
mongoIndexManagerError,
|
|
mongoIndexManagerSelectedName,
|
|
mongoIndexManagerMode,
|
|
mongoEditIndexOriginalName,
|
|
showClearElasticsearchIndexConfirm,
|
|
clearElasticsearchIndexLoading,
|
|
clearElasticsearchIndexTypedName,
|
|
showFlushRedisDbConfirm,
|
|
showCreateSchemaDialog,
|
|
createSchemaName,
|
|
showDropSchemaConfirm,
|
|
showEditDatabasePropertiesDialog,
|
|
editDatabasePropertiesLoading,
|
|
editDatabasePropertiesPreviewSql,
|
|
editDatabaseCharset,
|
|
editDatabaseCollation,
|
|
editDatabaseCommentText,
|
|
showEditSchemaCommentDialog,
|
|
showCompileErrorDialog,
|
|
compileErrorTitle,
|
|
compileErrorMessage,
|
|
schemaCommentText,
|
|
schemaCommentLoading,
|
|
schemaCommentPreviewSql,
|
|
showDeleteGroupConfirm,
|
|
deleteConnectionsWithGroup,
|
|
showMoveToNewGroupDialog,
|
|
moveToNewGroupName,
|
|
type DuplicateStructureSource,
|
|
} from "./sidebarTreeDialogState";
|
|
|
|
const { t, locale: appLocale } = useI18n();
|
|
|
|
const connectionStore = useConnectionStore();
|
|
|
|
const queryStore = useQueryStore();
|
|
|
|
const settingsStore = useSettingsStore();
|
|
|
|
const savedSqlStore = useSavedSqlStore();
|
|
|
|
const { toast } = useToast();
|
|
const installedPlugins = ref<InstalledPlugin[]>([]);
|
|
const sidebarPluginRegistry = computed(() => createFrontendPluginRegistry(installedPlugins.value, appLocale.value));
|
|
|
|
void api.listPlugins().then(
|
|
(plugins) => {
|
|
installedPlugins.value = plugins.filter((plugin) => plugin.compatibility.compatible);
|
|
},
|
|
() => {},
|
|
);
|
|
|
|
const { highlight } = useSqlHighlighter();
|
|
|
|
const { openData } = useSidebarDataOpenRuntime();
|
|
|
|
const { getDatabaseOptions } = useDatabaseOptions();
|
|
|
|
const props = defineProps<{
|
|
node: TreeNode;
|
|
depth: number;
|
|
dragDisabled?: boolean;
|
|
pendingRename?: boolean;
|
|
highlighted?: boolean;
|
|
}>();
|
|
|
|
const activeNode = shallowRef<TreeNode>(props.node);
|
|
let acceptedSelectionIds: readonly string[] | null = null;
|
|
let latestNavigationRequestId = 0;
|
|
|
|
function beginNavigationRequest(): number {
|
|
latestNavigationRequestId += 1;
|
|
return latestNavigationRequestId;
|
|
}
|
|
|
|
function isCurrentNavigationRequest(requestId: number): boolean {
|
|
return requestId === latestNavigationRequestId;
|
|
}
|
|
|
|
function releaseActiveNodeReference(nodeIds: readonly string[]) {
|
|
activeNode.value = releaseRemovedSidebarActionTarget(activeNode.value, nodeIds);
|
|
}
|
|
|
|
watch(
|
|
() => connectionStore.treeNodes,
|
|
(nodes) => {
|
|
const liveNode = findSidebarActionTarget(nodes, createSidebarActionTarget(activeNode.value));
|
|
activeNode.value = liveNode ?? releaseRemovedSidebarActionTarget(activeNode.value, [activeNode.value.id]);
|
|
},
|
|
{ flush: "post" },
|
|
);
|
|
|
|
const { copyStructureAs, copyStructureDocText, copyStructurePreview, exportData, exportDataXlsx, exportMongoCollection, exportStructure, saveStructurePreview, selectTextareaContent } = useSidebarTreeExportRuntime({
|
|
activeNode,
|
|
connectionStore,
|
|
settingsStore,
|
|
acceptedSelectionIds: () => acceptedSelectionIds,
|
|
});
|
|
|
|
const {
|
|
openAllDatabasesExport,
|
|
openDataCompare,
|
|
openDatabaseExport,
|
|
openDatabaseSearch,
|
|
openDiagram,
|
|
openDocs,
|
|
openFieldLineage,
|
|
openMongoImport,
|
|
openMongoDatabaseDump,
|
|
openScheduledBackups,
|
|
openSchemaDiff,
|
|
openSchemaDiffForRoutine,
|
|
openSqlFileExecution,
|
|
openStructureEditor,
|
|
openTableImport,
|
|
openTransfer,
|
|
} = useSidebarTreeToolRuntime({
|
|
activeNode,
|
|
connectionStore,
|
|
queryStore,
|
|
settingsStore,
|
|
tableChildObjectName: tableChildDropObjectName,
|
|
acceptedSelectionIds: () => acceptedSelectionIds,
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
"rename-started": [];
|
|
"request-connection-rename": [connectionId: string];
|
|
"request-group-rename": [groupId: string];
|
|
"request-saved-sql-rename": [nodeId: string];
|
|
"node-toggled": [node: TreeNode, expanded: boolean];
|
|
"search-toggle": [node: TreeNode];
|
|
"context-menu": [event: MouseEvent, node: TreeNode, items: ContextMenuItem[]];
|
|
"open-ddl": [node: TreeNode];
|
|
"open-elasticsearch-index-metadata": [node: TreeNode, kind: ElasticsearchIndexMetadataKind];
|
|
"open-object-source": [node: TreeNode, initialEditing: boolean];
|
|
"open-procedure": [node: TreeNode];
|
|
"open-settings": [initialTab: string];
|
|
"open-data": [node: TreeNode, requireSelection: boolean, openMode: DataTabOpenMode, runner: (node: TreeNode, request: SidebarDataOpenRequest) => Promise<void>];
|
|
"open-visible-databases": [node: TreeNode];
|
|
"open-visible-schemas": [node: TreeNode];
|
|
"open-visible-nacos-namespaces": [node: TreeNode];
|
|
"open-table-name-filters": [node: TreeNode];
|
|
"add-to-ai": [nodes: TreeNode | TreeNode[]];
|
|
"open-danger-dialog": [request: SidebarDangerDialogRequest];
|
|
"open-dialog-controller": [controller: Record<string, any> | null];
|
|
"open-install-extension": [node: TreeNode];
|
|
"open-extension-details": [node: TreeNode];
|
|
}>();
|
|
|
|
const {
|
|
setNodeAsDefaultDatabase,
|
|
clearNodeDefaultDatabase,
|
|
setNodeAsDefaultSchema,
|
|
clearNodeDefaultSchema,
|
|
connectionDeleteMenuLabel,
|
|
connectionDuplicateMenuLabel,
|
|
connectionDeleteConfirmMessage,
|
|
deleteConnection,
|
|
confirmDelete,
|
|
copyFinalProxyPort,
|
|
duplicateConnection,
|
|
editConnection,
|
|
revealConnectionFilePath,
|
|
revealDatabaseFile,
|
|
canBackupSqliteDatabase,
|
|
backupSqliteDatabase,
|
|
restoreSqliteDatabase,
|
|
disconnectConnection,
|
|
connectionDisconnectMenuLabel,
|
|
canDisconnectConnection,
|
|
connectionGroupDisconnectMenuLabel,
|
|
canDisconnectConnectionGroup,
|
|
disconnectConnectionGroup,
|
|
canForgetSessionCredential,
|
|
disconnectAndForgetConnectionPassword,
|
|
cancelConnectionAttempt,
|
|
closeDatabaseConnection,
|
|
isPinned,
|
|
isNodeDefaultDatabase,
|
|
isNodeDefaultSchema,
|
|
isConnected,
|
|
isConnecting,
|
|
canCloseDatabaseConnection,
|
|
canConfigureVisibleDatabases,
|
|
canConfigureVisibleSchemas,
|
|
canCopyFinalProxyPort,
|
|
togglePin,
|
|
openVisibleDatabasesDialog,
|
|
openVisibleSchemasDialog,
|
|
startRenameGroup,
|
|
connectionGroupDeleteMenuLabel,
|
|
connectionGroupDeleteConfirmMessage,
|
|
deleteConnectionGroup,
|
|
newConnectionInGroup,
|
|
newSubgroup,
|
|
confirmDeleteGroup,
|
|
deletingConnectionGroups,
|
|
moveToGroup,
|
|
createGroupAndMoveConnection,
|
|
} = useSidebarConnectionMutationRuntime({
|
|
activeNode,
|
|
releaseActiveNodeReference,
|
|
selectedTreeNodesInVisibleOrder,
|
|
connectionStore,
|
|
queryStore,
|
|
requestGroupRename: (groupId) => emit("request-group-rename", groupId),
|
|
openVisibleDatabases: (node) => emit("open-visible-databases", node),
|
|
openVisibleSchemas: (node) => emit("open-visible-schemas", node),
|
|
});
|
|
|
|
const {
|
|
canDropMongoDatabase,
|
|
canDropMilvusDatabase,
|
|
canDropMongoCollection,
|
|
canDropMilvusCollection,
|
|
canRenameMongoCollection,
|
|
canCloneMongoCollection,
|
|
prepareRenameMongoCollectionDialog,
|
|
confirmRenameMongoCollection,
|
|
showRenameMongoCollectionDialog,
|
|
renameMongoCollectionName,
|
|
renameMongoCollectionError,
|
|
renameMongoCollectionPreview,
|
|
renameMongoCollectionLoading,
|
|
prepareCloneMongoCollectionDialog,
|
|
confirmCloneMongoCollection,
|
|
showCloneMongoCollectionDialog,
|
|
cloneMongoCollectionName,
|
|
cloneMongoCollectionError,
|
|
cloneMongoCollectionLoading,
|
|
mongoIndexNameForNode,
|
|
canDropMongoIndexNode,
|
|
canDropMongoIndex,
|
|
canDropAllMongoIndexes,
|
|
mongoIndexDropPreview,
|
|
mongoDropAllIndexesPreview,
|
|
refreshMongoIndexTreeAfterMutation,
|
|
canCreateMongoIndex,
|
|
mongoIndexKeyTypes,
|
|
mongoCreateIndexCanSubmit,
|
|
mongoCreateIndexCanAddField,
|
|
prepareCreateMongoIndexDialog,
|
|
canCreateMeilisearchIndex,
|
|
prepareCreateMeilisearchIndexDialog,
|
|
confirmCreateMeilisearchIndex,
|
|
addMongoCreateIndexField,
|
|
removeMongoCreateIndexField,
|
|
confirmCreateMongoIndex,
|
|
canManageMongoIndexes,
|
|
prepareMongoIndexManagerDialog,
|
|
loadMongoIndexManagerRows,
|
|
mongoIndexManagerSelected,
|
|
mongoIndexManagerCollectionName,
|
|
selectMongoIndexRow,
|
|
startCreateMongoIndexDraft,
|
|
startEditMongoIndexDraft,
|
|
cancelMongoIndexDraft,
|
|
dropSelectedMongoIndexRow,
|
|
canDropSelectedMongoIndexRow,
|
|
canEditSelectedMongoIndexRow,
|
|
confirmEditMongoIndex,
|
|
openCreateNacosNamespaceDialog,
|
|
confirmCreateNacosNamespace,
|
|
openEditNacosNamespaceDialog,
|
|
confirmEditNacosNamespace,
|
|
openDeleteNacosNamespaceConfirm,
|
|
confirmDeleteNacosNamespace,
|
|
dropMongoCollection,
|
|
dropMilvusCollection,
|
|
dropMongoIndex,
|
|
dropAllMongoIndexes,
|
|
canManageElasticsearchIndex,
|
|
clearElasticsearchIndex,
|
|
confirmClearElasticsearchIndex,
|
|
flushRedisDb,
|
|
prepareRedisDatabaseAliasDialog,
|
|
confirmRedisDatabaseAlias,
|
|
clearRedisDatabaseAlias,
|
|
showRedisDatabaseAliasDialog,
|
|
redisDatabaseAliasInput,
|
|
redisDatabaseAliasSaving,
|
|
confirmFlushRedisDb,
|
|
confirmDropMongoDatabase,
|
|
confirmDropMongoCollection,
|
|
confirmDropMilvusDatabase,
|
|
confirmDropMilvusCollection,
|
|
confirmDropMongoIndex,
|
|
confirmDropAllMongoIndexes,
|
|
} = useSidebarDatabaseSpecificMutationRuntime({ activeNode, connectionStore });
|
|
|
|
const {
|
|
isTableNotView,
|
|
supportsTruncate,
|
|
supportsVacuum,
|
|
supportsMysqlAutoIncrement,
|
|
canDropTableCascade,
|
|
canTruncateTableCascade,
|
|
refreshDropTablePreviewSql,
|
|
refreshTruncateTablePreviewSql,
|
|
dropTable,
|
|
refreshTableList,
|
|
confirmDropTable,
|
|
emptyTable,
|
|
confirmEmptyTable,
|
|
truncateTable,
|
|
confirmTruncateTable,
|
|
vacuumTable,
|
|
refreshVacuumPreviewForOptions,
|
|
confirmVacuumTable,
|
|
mysqlAutoIncrement,
|
|
refreshMysqlAutoIncrementPreviewSql,
|
|
confirmMysqlAutoIncrement,
|
|
} = useSidebarTableMutationRuntime({
|
|
activeNode,
|
|
releaseActiveNodeReference,
|
|
connectionStore,
|
|
currentDatabaseType,
|
|
databaseTypeForNode,
|
|
executeWithProductionGuard: executeTreeNodeSqlWithProductionGuard,
|
|
closeDroppedTableObjectTabsForNode,
|
|
refreshMutatedTableDataTabsForNode,
|
|
});
|
|
|
|
const batchDropProgress = ref({ completed: 0, total: 0 });
|
|
|
|
const treeItemDialogOwner = Symbol("sidebar-tree-dialog-owner");
|
|
|
|
function claimTreeItemDialogOwnership() {
|
|
sidebarTreeDialogOwner.value = treeItemDialogOwner;
|
|
}
|
|
|
|
function routeTreeItemDialogController() {
|
|
const controller = getTreeItemDialogController();
|
|
const target = createSidebarActionTarget(activeNode.value);
|
|
sidebarFormTarget.value = target;
|
|
const routedController = createRoutedSidebarDialogController(controller, {
|
|
node: target,
|
|
wrapAction: (action) => {
|
|
return (...args: unknown[]) => {
|
|
activateActionTarget(target);
|
|
return action(...args);
|
|
};
|
|
},
|
|
});
|
|
routedController.pasteTableDataCopySupported = pasteTableDataCopySupported.value;
|
|
routedController.canSetCreateDatabaseCharset = canSetCreateDatabaseCharset.value;
|
|
routedController.canEditDatabaseCharsetCollation = canEditDatabaseCharsetCollation.value;
|
|
routedController.canEditDatabaseComment = canEditDatabaseComment.value;
|
|
emit("open-dialog-controller", routedController);
|
|
}
|
|
|
|
const sidebarTreeContext = inject(sidebarTreeContextKey, null);
|
|
|
|
function shouldReleaseCollapsedTreeNodeChildren(): boolean {
|
|
return !connectionStore.sidebarSearchQuery && !sidebarTreeContext?.isSearchProjectionActive?.();
|
|
}
|
|
|
|
function currentDatabaseType(): DatabaseType | undefined {
|
|
return activeNode.value.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(activeNode.value.connectionId)) : undefined;
|
|
}
|
|
|
|
function currentTableStructureDatabaseType(): DatabaseType | undefined {
|
|
return activeNode.value.connectionId ? tableStructureDatabaseTypeForConnection(connectionStore.getConfig(activeNode.value.connectionId)) : undefined;
|
|
}
|
|
|
|
function rawDatabaseType(): DatabaseType | undefined {
|
|
return activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId)?.db_type : undefined;
|
|
}
|
|
|
|
function databaseTypeForNode(node: TreeNode): DatabaseType | undefined {
|
|
return node.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
|
|
}
|
|
|
|
function sidebarTableCopyFormatOptions(node: TreeNode = activeNode.value): FormatSidebarTableNamesOptions {
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
return {
|
|
separator: settingsStore.editorSettings.sidebarCopyTableNameSeparator,
|
|
includeSchema: settingsStore.editorSettings.sidebarCopyTableNameIncludeSchema,
|
|
databaseType: node.connectionId ? effectiveDatabaseTypeForConnection(config) : undefined,
|
|
driverProfile: config?.driver_profile,
|
|
identifierQuote: node.connectionId ? connectionStore.connectionIdentifierQuote?.(node.connectionId) : undefined,
|
|
};
|
|
}
|
|
|
|
function formatSelectedTableNamesForClipboard(selectedNodes: readonly TreeNode[] = selectedTreeNodesInVisibleOrder()): string {
|
|
return formatSidebarTableCopyText(activeNode.value, selectedNodes, sidebarTableCopyFormatOptions());
|
|
}
|
|
|
|
function tableStructureDatabaseTypeForNode(node: TreeNode): DatabaseType | undefined {
|
|
return node.connectionId ? tableStructureDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
|
|
}
|
|
|
|
function hasNodeDatabaseContext(node: TreeNode): node is TreeNode & { connectionId: string; database: string } {
|
|
return !!node.connectionId && hasTreeNodeDatabaseContext(node);
|
|
}
|
|
|
|
const groupTypes: Set<TreeNodeType> = new Set([
|
|
"group-columns",
|
|
"group-indexes",
|
|
"group-fkeys",
|
|
"group-triggers",
|
|
"group-events",
|
|
"group-constraints",
|
|
"group-table-partitions",
|
|
"group-table-subpartitions",
|
|
"group-tables",
|
|
"group-views",
|
|
"group-materialized-views",
|
|
"group-procedures",
|
|
"group-functions",
|
|
"group-sequences",
|
|
"group-synonyms",
|
|
"group-jobs",
|
|
"group-packages",
|
|
"group-types",
|
|
"group-partitions",
|
|
"group-extensions",
|
|
"group-tablespaces",
|
|
"group-datafiles",
|
|
]);
|
|
|
|
function isGroupLabel(node: TreeNode): boolean {
|
|
return groupTypes.has(node.type);
|
|
}
|
|
|
|
async function openDirectNavigationNode(node: TreeNode, requestId: number) {
|
|
if (!node.connectionId) return;
|
|
const isNacosNavigation = node.type === "nacos-namespace" || node.type === "nacos-access-control";
|
|
const isEtcdNavigation = node.type === "etcd-root" || node.type === "etcd-dashboard" || node.type === "etcd-access-control";
|
|
await connectionStore.ensureConnected(node.connectionId, isNacosNavigation || isEtcdNavigation ? { verifyHealth: false } : undefined);
|
|
if (!isCurrentNavigationRequest(requestId)) return;
|
|
if ((node.type === "etcd-dashboard" || node.type === "etcd-access-control") && !connectionStore.getEtcdAccessCapabilities(node.connectionId).admin) return;
|
|
const connectionName = connectionStore.getConfig(node.connectionId)?.name || (isNacosNavigation ? "Nacos" : isEtcdNavigation ? "etcd" : "Consul");
|
|
if (node.type === "consul-root") {
|
|
queryStore.createTab(node.connectionId, "", `${connectionName}:keys`, "consul");
|
|
refreshActiveKvBrowserAfterOpen("consul", node.connectionId);
|
|
} else if (node.type === "consul-overview") {
|
|
queryStore.createTab(node.connectionId, "", `${connectionName}:${t("consul.ui.overview")}`, "consul-overview");
|
|
} else if (node.type === "etcd-root") {
|
|
queryStore.createTab(node.connectionId, "", `${connectionName}:keys`, "etcd");
|
|
refreshActiveKvBrowserAfterOpen("etcd", node.connectionId);
|
|
} else if (node.type === "etcd-dashboard") {
|
|
queryStore.createTab(node.connectionId, "", `${connectionName}:dashboard`, "etcd-dashboard");
|
|
} else if (node.type === "etcd-access-control") {
|
|
queryStore.createTab(node.connectionId, "", `${connectionName}:access-control`, "etcd-access-control");
|
|
} else if (node.type === "nacos-namespace") {
|
|
queryStore.openNacosAdmin(node.connectionId, { namespace: node.nacosNamespace || "", namespaceName: node.nacosNamespaceName || node.label });
|
|
} else if (node.type === "nacos-access-control") {
|
|
queryStore.createTab(node.connectionId, "", `${connectionName}:access-control`, "nacos-access-control");
|
|
} else if (node.type === "meilisearch-system") {
|
|
queryStore.createTab(node.connectionId, "default", t("meilisearch.systemManagement"), "meilisearch-system");
|
|
}
|
|
}
|
|
|
|
async function toggle(requestId = beginNavigationRequest()) {
|
|
const node = activeNode.value;
|
|
// Remote sidebar search intentionally descends into one active connection.
|
|
// Keep that scope aligned even when this toggle is satisfied entirely from
|
|
// already-loaded children and therefore performs no connection request.
|
|
if (node.connectionId && connectionStore.connectedIds.has(node.connectionId)) {
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
}
|
|
const treeLoadSearchOptions = sidebarTreeContext?.getTreeLoadSearchOptions?.(node);
|
|
if (isDirectNavigationTreeNode(node.type)) {
|
|
try {
|
|
await openDirectNavigationNode(node, requestId);
|
|
} catch (e: any) {
|
|
if (!isCurrentNavigationRequest(requestId)) return;
|
|
const errMsg = e?.message || String(e);
|
|
if (errMsg.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return;
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
openDriverStoreForInstallError(errMsg, node);
|
|
}
|
|
return;
|
|
}
|
|
// PostgreSQL-family custom types without members (enum/domain/range/base) do
|
|
// not toggle; only composite types expand into their member list.
|
|
if (node.type === "type" && customTypeCapabilities(currentDatabaseType()).details && node.hasMembers === false) return;
|
|
if (node.isLoading) {
|
|
if (node.isExpanded) {
|
|
node.isExpanded = false;
|
|
if (shouldReleaseCollapsedTreeNodeChildren()) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
|
|
connectionStore.cancelTreeNodeLoad(node.id);
|
|
emitNodeToggled(node, true, false);
|
|
} else {
|
|
node.isExpanded = true;
|
|
emitNodeToggled(node, false);
|
|
}
|
|
return;
|
|
}
|
|
emit("search-toggle", node);
|
|
const wasExpanded = !!node.isExpanded;
|
|
|
|
if (node.type === "connection-group") {
|
|
node.isExpanded = !node.isExpanded;
|
|
connectionStore.toggleConnectionGroupCollapsed(node.id);
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.type === "group-partitions") {
|
|
node.isExpanded = !node.isExpanded;
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.type === "table-vgroup") {
|
|
node.isExpanded = !node.isExpanded;
|
|
if (node.vgroupId) connectionStore.toggleTableVGroupCollapsed(node, node.vgroupId);
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.type === "type" && customTypeCapabilities(currentDatabaseType()).details && node.children !== undefined) {
|
|
node.isExpanded = node.children.length > 0 ? !node.isExpanded : false;
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
// Xugu package members are represented with the same visual group types as
|
|
// schema-level procedures/functions, but their children are already loaded
|
|
// from the owning package specification. Keep this scoped group local:
|
|
// routing it through loadSidebarObjectGroup would query every routine in the
|
|
// schema and replace the package's member list with unrelated objects.
|
|
if (currentDatabaseType() === "xugu" && (node.type === "group-procedures" || node.type === "group-functions") && node.parentType === "package") {
|
|
node.isExpanded = !node.isExpanded;
|
|
// Package member groups have no group-level reload path: their children
|
|
// are hydrated together with the owning package. Releasing a large group
|
|
// here would leave it empty on the next local-only expand.
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
// Keep the click path aligned with every object-group definition. In
|
|
// particular, schema-level trigger/type groups have no tableName, so they
|
|
// must use the generic object loader rather than the table-trigger loader.
|
|
const databaseObjectGroup = !!objectTypesForGroupNode(node.type);
|
|
if (databaseObjectGroup && connectionStore.canUseLoadedTreeNodeToggle(node)) {
|
|
node.isExpanded = !node.isExpanded;
|
|
if (wasExpanded && shouldReleaseCollapsedTreeNodeChildren()) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.type === "type-attributes" || node.type === "type-methods") {
|
|
node.isExpanded = !node.isExpanded;
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.type === "saved-sql-root" || node.type === "saved-sql-folder") {
|
|
node.isExpanded = !node.isExpanded;
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.type === "group-extensions" && connectionStore.canUseLoadedTreeNodeToggle(node)) {
|
|
node.isExpanded = !node.isExpanded;
|
|
if (wasExpanded && shouldReleaseCollapsedTreeNodeChildren()) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
// Xugu tablespace rows and their datafile groups are eagerly materialized
|
|
// with the tablespace listing; expanding them is a pure local toggle.
|
|
if (node.type === "tablespace" || node.type === "group-datafiles") {
|
|
node.isExpanded = !node.isExpanded;
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.type === "group-tablespaces") {
|
|
if (connectionStore.canUseLoadedTreeNodeToggle(node)) {
|
|
node.isExpanded = !node.isExpanded;
|
|
if (wasExpanded && shouldReleaseCollapsedTreeNodeChildren()) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
try {
|
|
await connectionStore.loadXuguTablespaces(node, { preserveCollapsedChildren: !shouldReleaseCollapsedTreeNodeChildren() });
|
|
emitNodeToggled(node, wasExpanded);
|
|
} catch (e: any) {
|
|
if (!isCurrentNavigationRequest(requestId)) return;
|
|
if (!wasExpanded) node.isExpanded = false;
|
|
const errMsg = e?.message || String(e);
|
|
if (errMsg.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return;
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
openDriverStoreForInstallError(errMsg);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isXuguTypeMemberContainer(node, connectionStore.getConfig(node.connectionId || "")?.db_type)) {
|
|
await connectionStore.loadXuguTypeMembers(node, { preserveCollapsedChildren: !shouldReleaseCollapsedTreeNodeChildren() });
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.isExpanded) {
|
|
node.isExpanded = false;
|
|
if (shouldReleaseCollapsedTreeNodeChildren()) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
|
|
emitNodeToggled(node, wasExpanded, false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (node.type === "type" && customTypeCapabilities(currentDatabaseType()).details) {
|
|
await connectionStore.loadCustomTypeChildren(node);
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (await loadSidebarObjectGroup(node, connectionStore, treeLoadSearchOptions)) {
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
const packageDatabaseType = currentDatabaseType();
|
|
if (node.type === "package" && node.connectionId && supportsPackageMemberExpansion(packageDatabaseType, packageDatabaseType === "opengauss" ? connectionStore.databaseCompatibilityMode(node.connectionId, node.database) : undefined)) {
|
|
await connectionStore.loadPackageMembers(node);
|
|
emitNodeToggled(node, wasExpanded);
|
|
return;
|
|
}
|
|
|
|
if (node.type === "connection" && node.connectionId) {
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
if (config?.db_type === "redis") {
|
|
await connectionStore.loadRedisDatabases(node.connectionId);
|
|
} else if (config?.db_type === "etcd") {
|
|
await connectionStore.loadEtcdRoot(node.connectionId);
|
|
} else if (config?.db_type === "zookeeper") {
|
|
await connectionStore.loadZooKeeperRoot(node.connectionId);
|
|
} else if (config?.db_type === "consul") {
|
|
await connectionStore.loadConsulRoot(node.connectionId);
|
|
} else if (config?.db_type === "mongodb") {
|
|
await connectionStore.loadMongoDatabases(node.connectionId);
|
|
} else if (config?.db_type === "dynamodb") {
|
|
await connectionStore.loadDynamoDbTables(node.connectionId);
|
|
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
|
// Expand: list indices (like other db types list databases).
|
|
await connectionStore.loadElasticsearchIndices(node.connectionId);
|
|
} else if (config?.db_type === "milvus") {
|
|
await connectionStore.loadMilvusDatabases(node.connectionId);
|
|
} else if (config?.db_type === "qdrant" || config?.db_type === "weaviate" || config?.db_type === "chromadb") {
|
|
await connectionStore.loadVectorCollections(node.connectionId);
|
|
} else if (config?.db_type === "mq") {
|
|
await connectionStore.loadMqTenants(node.connectionId);
|
|
} else if (config?.db_type === "nacos") {
|
|
await connectionStore.loadNacosNamespaces(node.connectionId, treeLoadSearchOptions);
|
|
} else if (config?.db_type === "mqtt") {
|
|
await connectionStore.loadMqttTopics(node.connectionId);
|
|
} else if (config?.db_type === "plugin") {
|
|
await queryStore.openPluginConnection(node.connectionId);
|
|
return;
|
|
} else {
|
|
await connectionStore.loadDatabases(node.connectionId, treeLoadSearchOptions);
|
|
}
|
|
} else if (node.type === "redis-db" && node.connectionId && node.database) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const alias = connectionStore.getRedisDatabaseAlias(node.connectionId, node.database);
|
|
const databaseLabel = alias ? `db${node.database} · ${alias}` : `db${node.database}`;
|
|
const tabTitle = `${connectionStore.getConfig(node.connectionId)?.name || "Redis"}:${databaseLabel}`;
|
|
queryStore.createTab(node.connectionId, node.database, tabTitle, "redis");
|
|
} else if (node.type === "mq-tenant" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
queryStore.openMqAdmin(node.connectionId, { tenant: node.mqTenant || node.label, initialTab: node.mqInitialTab });
|
|
} else if (node.type === "mqtt-topic" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const topicFromId = node.id.endsWith(":mqtt-topic:__console__") ? undefined : node.id.split(":mqtt-topic:")[1] || node.label;
|
|
queryStore.openMqttAdmin(node.connectionId, topicFromId ? { initialTopic: topicFromId } : undefined);
|
|
} else if (node.type === "zookeeper-root" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const tabTitle = `${connectionStore.getConfig(node.connectionId)?.name || "ZooKeeper"}:keys`;
|
|
queryStore.createTab(node.connectionId, "", tabTitle, "zookeeper");
|
|
refreshActiveKvBrowserAfterOpen("zookeeper", node.connectionId);
|
|
} else if (node.type === "user-admin" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
queryStore.openUserAdmin(node.connectionId);
|
|
} else if (node.type === "dameng-users" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
queryStore.openDamengUsers(node.connectionId);
|
|
} else if (node.type === "dameng-roles" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
queryStore.openDamengRoles(node.connectionId);
|
|
} else if (node.type === "dameng-job-admin" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
queryStore.openDamengJobAdmin(node.connectionId);
|
|
} else if (node.type === "mongo-db" && node.connectionId && node.database) {
|
|
await connectionStore.loadMongoCollections(node.connectionId, node.database);
|
|
} else if (node.type === "vector-database" && node.connectionId && node.database) {
|
|
await connectionStore.loadVectorCollections(node.connectionId, node.database);
|
|
} else if (node.type === "mongo-collection" && node.connectionId && node.database) {
|
|
await connectionStore.loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id);
|
|
} else if (node.type === "elasticsearch-index" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
if (connectionStore.getConfig(node.connectionId)?.db_type === "meilisearch") {
|
|
const tab = queryStore.createTab(node.connectionId, node.database || "default", node.label, "meilisearch");
|
|
queryStore.updateSql(tab, node.label);
|
|
} else {
|
|
const tab = queryStore.createTab(node.connectionId, node.database || "default", node.label, "mongo");
|
|
queryStore.updateSql(tab, node.label);
|
|
}
|
|
} else if (node.type === "vector-collection" && node.connectionId) {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const collectionRef = (node.meta as { collectionId?: string } | undefined)?.collectionId ?? node.label;
|
|
const tab = queryStore.createTab(node.connectionId, node.database || "default", node.label, "vector");
|
|
queryStore.updateSql(tab, collectionRef);
|
|
if (connectionStore.getConfig(node.connectionId)?.db_type !== "milvus") {
|
|
api
|
|
.vectorGetCollectionDetail(node.connectionId, node.database || "default", collectionRef)
|
|
.then((info) => {
|
|
if (info.dimension == null) return;
|
|
if (node.meta) {
|
|
(node.meta as Record<string, unknown>).dimension = info.dimension;
|
|
} else {
|
|
node.meta = { dimension: info.dimension } as any;
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
|
if (node.catalog && node.catalog !== "internal") {
|
|
await connectionStore.loadDorisCatalogTables(node, treeLoadSearchOptions);
|
|
} else {
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
|
|
if (config?.db_type === "sqlserver") {
|
|
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database, treeLoadSearchOptions);
|
|
} else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
|
|
await connectionStore.loadSchemas(node.connectionId, node.database, treeLoadSearchOptions);
|
|
} else {
|
|
await connectionStore.loadTables(node.connectionId, node.database, undefined, treeLoadSearchOptions);
|
|
}
|
|
}
|
|
} else if (node.type === "doris-catalog" && node.connectionId) {
|
|
await connectionStore.loadDorisCatalogDatabases(node, treeLoadSearchOptions);
|
|
} else if (node.type === "schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && schemaNodeHasLoadableName(effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)), node.schema)) {
|
|
await connectionStore.loadTables(node.connectionId, node.database, node.schema, treeLoadSearchOptions);
|
|
} else if (node.type === "oracle-db-links") {
|
|
await connectionStore.loadTreeNodeChildren(node, treeLoadSearchOptions);
|
|
} else if (node.type === "linked-server-root" && node.connectionId) {
|
|
await connectionStore.loadSqlServerLinkedServers(node.connectionId, treeLoadSearchOptions);
|
|
} else if (node.type === "linked-server" && node.connectionId) {
|
|
await connectionStore.loadSqlServerLinkedServerCatalogs(node, treeLoadSearchOptions);
|
|
} else if (node.type === "linked-server-catalog" && node.connectionId) {
|
|
await connectionStore.loadSqlServerLinkedServerSchemas(node, treeLoadSearchOptions);
|
|
} else if (node.type === "linked-server-schema" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.schema) {
|
|
await connectionStore.loadTables(node.connectionId, node.database, node.schema, treeLoadSearchOptions);
|
|
} else if ((node.type === "table" || node.type === "view" || node.type === "materialized_view") && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
|
await connectionStore.loadTableGroups(node.connectionId, node.database, node.label, node.schema, node.id, node.catalog);
|
|
} else if (node.type === "group-columns" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
|
await connectionStore.loadColumns(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
|
} else if (node.type === "group-indexes" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
|
await connectionStore.loadIndexes(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
|
} else if (node.type === "group-fkeys" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
|
await connectionStore.loadForeignKeys(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
|
} else if (node.type === "group-triggers" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
|
await connectionStore.loadTriggers(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
|
} else if (node.type === "group-constraints" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
|
await connectionStore.loadConstraints(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
|
} else if (node.type === "group-table-partitions" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
|
await connectionStore.loadPartitions(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
|
} else if (node.type === "group-table-subpartitions" && node.connectionId && hasTreeNodeDatabaseContext(node) && node.tableName) {
|
|
await connectionStore.loadSubpartitions(node.connectionId, node.database, node.tableName, node.schema, node.id, node.catalog);
|
|
} else if (node.type === "group-extensions" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
|
|
await connectionStore.refreshTreeNode(node);
|
|
}
|
|
emitNodeToggled(node, wasExpanded);
|
|
} catch (e: any) {
|
|
if (!isCurrentNavigationRequest(requestId)) return;
|
|
if (!wasExpanded) node.isExpanded = false;
|
|
const errMsg = e?.message || String(e);
|
|
if (errMsg.includes(CONNECTION_ATTEMPT_CANCELLED_MESSAGE)) return;
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
openDriverStoreForInstallError(errMsg);
|
|
}
|
|
}
|
|
|
|
function runRowClickAction(clickDetail: number, requestId: number) {
|
|
const node = activeNode.value;
|
|
if (node.type === "load-more") {
|
|
if (clickDetail > 1) return;
|
|
void loadMoreObjectGroupChildren();
|
|
return;
|
|
}
|
|
if (node.type === "object-browser") {
|
|
if (clickDetail > 1) return;
|
|
void openObjectBrowser();
|
|
return;
|
|
}
|
|
if (node.type === "mongo-gridfs") {
|
|
openMongoTreeData(node);
|
|
return;
|
|
}
|
|
if (node.type === "event") {
|
|
void openObjectBrowser(false, true);
|
|
return;
|
|
}
|
|
if (node.type === "group-events") {
|
|
void openObjectBrowser();
|
|
return;
|
|
}
|
|
const action = treeNodeRowAction(node.type, canExpand.value, settingsStore.editorSettings.sidebarActivation, currentDatabaseType(), settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation, canOpenObjectBrowser.value);
|
|
// WebKit can keep incrementing click.detail while the pointer moves quickly
|
|
// between adjacent rows. Nacos entries are idempotent navigation targets, so
|
|
// do not mistake that rapid one-click switching for a double-click toggle.
|
|
if (!shouldRunTreeNodeRowAction(action, clickDetail, isGroupLabel(node) || isRepeatableNavigationTreeNode(node.type))) return;
|
|
if (action === "open-data") {
|
|
scheduleOpenData(node);
|
|
} else if (action === "open-object-browser") {
|
|
void openObjectBrowser(false, false, true);
|
|
} else if (action === "open-object-browser-and-expand") {
|
|
void openObjectBrowser(false, false, true);
|
|
if (!node.isExpanded) void toggle();
|
|
} else if (action === "open-source") {
|
|
openObjectSourceDialog(false);
|
|
} else if (action === "open-extension-details") {
|
|
emit("open-extension-details", node);
|
|
} else if (action === "open-saved-sql") {
|
|
void openSavedSqlFile();
|
|
} else if (isDocumentBrowserTreeNode(node.type)) {
|
|
openMongoTreeData(node);
|
|
} else if (action === "toggle") {
|
|
void toggle(requestId);
|
|
}
|
|
}
|
|
|
|
function refreshActiveKvBrowserAfterOpen(mode: "etcd" | "zookeeper" | "consul", connectionId: string) {
|
|
void nextTick(() => {
|
|
window.dispatchEvent(new CustomEvent("dbx-refresh-active-kv-browser", { detail: { mode, connectionId } }));
|
|
});
|
|
}
|
|
|
|
function openDriverStoreForInstallError(errMsg: string, node: TreeNode = activeNode.value) {
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
const focus = driverStoreFocusForInstallError(errMsg, config?.db_type, config?.driver_profile);
|
|
if (focus) window.dispatchEvent(new CustomEvent("dbx-open-driver-store", { detail: focus }));
|
|
}
|
|
|
|
async function loadMoreObjectGroupChildren() {
|
|
const node = activeNode.value;
|
|
const searchFilter = node.loadMore?.parentId ? connectionStore.sidebarTableSearchQueries[node.loadMore.parentId]?.trim() || "" : "";
|
|
try {
|
|
await connectionStore.loadMoreObjectGroupChildren(node, { searchFilter });
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function loadAllObjectGroupChildren() {
|
|
const node = activeNode.value;
|
|
try {
|
|
await connectionStore.loadAllObjectGroupChildren(node);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
function visibleTreeNodes(): TreeNode[] {
|
|
if (sidebarTreeContext) return sidebarTreeContext.getVisibleNodes();
|
|
return flattenTree(connectionStore.treeNodes).map((item) => item.node);
|
|
}
|
|
|
|
function selectedTreeNodesInVisibleOrder(): TreeNode[] {
|
|
return orderSelectedTreeNodes(visibleTreeNodes(), acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds);
|
|
}
|
|
|
|
function isEditableShortcutTarget(target: EventTarget | null): boolean {
|
|
if (!(target instanceof HTMLElement)) return false;
|
|
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.isContentEditable || !!target.closest("[contenteditable='true']");
|
|
}
|
|
|
|
function onKeydown(event: KeyboardEvent) {
|
|
claimTreeItemDialogOwnership();
|
|
if ((!isSelected.value && !isMultiSelected.value) || isEditableShortcutTarget(event.target)) return;
|
|
if (isPasteTreeClipboardShortcut(event)) {
|
|
if (!requestPasteTreeClipboard()) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
if (isEditConnectionShortcut(event)) {
|
|
if (!requestEditSelectedConnection()) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
if (isSidebarTreeArrowKey(event) && handleSidebarTreeArrowKey(event)) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "F2") {
|
|
if (!requestRenameSelectedNode()) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
if (isModRShortcut(event) || (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "F5")) {
|
|
if (!requestRefreshSelectedNode()) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
if (
|
|
handleSidebarTreeDeleteShortcut(event, {
|
|
activeNode: activeNode.value,
|
|
selectedNodes: selectedTreeNodesInVisibleOrder(),
|
|
databaseTypeForNode,
|
|
requestHBaseTableDelete: () => {
|
|
ensureDangerDialogRouting();
|
|
routeTreeItemDialogController();
|
|
requestDeleteHBaseTable();
|
|
return true;
|
|
},
|
|
requestDefaultDelete: requestDeleteSelectedNode,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
if (!isCopyTreeSelectionShortcut(event)) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
copySelectedNames();
|
|
}
|
|
|
|
function isPasteTreeClipboardShortcut(event: KeyboardEvent): boolean {
|
|
return isPasteSidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts);
|
|
}
|
|
|
|
function isSidebarTreeArrowKey(event: KeyboardEvent): boolean {
|
|
return !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "ArrowLeft" || event.key === "ArrowRight");
|
|
}
|
|
|
|
function handleSidebarTreeArrowKey(event: KeyboardEvent): boolean {
|
|
const rows = sidebarTreeContext?.getVisibleFlatNodes?.();
|
|
if (!rows?.length) return false;
|
|
const action = sidebarTreeArrowAction(rows, activeNode.value.id, event.key, { databaseType: currentDatabaseType() });
|
|
if (action.kind === "none") return false;
|
|
if (action.kind === "toggle") {
|
|
toggleNode(activeNode.value);
|
|
return true;
|
|
}
|
|
connectionStore.connectionMultiSelectActive = false;
|
|
connectionStore.selectedTreeNodeId = action.nodeId;
|
|
connectionStore.selectedTreeNodeIds = [action.nodeId];
|
|
connectionStore.treeSelectionAnchorId = action.nodeId;
|
|
sidebarTreeContext?.focusTreeNode?.(action.nodeId);
|
|
return true;
|
|
}
|
|
|
|
function isEditConnectionShortcut(event: KeyboardEvent): boolean {
|
|
return isEditSidebarConnectionShortcut(event, settingsStore.editorSettings.shortcuts);
|
|
}
|
|
|
|
function isCopyTreeSelectionShortcut(event: KeyboardEvent): boolean {
|
|
return isCopySidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts);
|
|
}
|
|
|
|
function pasteTableTargetContext(): TableClipboardContext | null {
|
|
if (!activeNode.value.connectionId || !activeNode.value.database) return null;
|
|
return {
|
|
connectionId: activeNode.value.connectionId,
|
|
database: activeNode.value.database,
|
|
schema: normalizeTreeClipboardSchema(activeNode.value.connectionId, activeNode.value.database, activeNode.value.schema),
|
|
};
|
|
}
|
|
|
|
function normalizeTreeClipboardSchema(connectionId: string, database: string, schema?: string): string | undefined {
|
|
return connectionObjectTreeNodeSchema(connectionStore.getConfig(connectionId), database, schema);
|
|
}
|
|
|
|
function normalizedTreeClipboardTableEntries(): TableClipboardTableContext[] {
|
|
const clipboard = connectionStore.treeClipboard;
|
|
if (clipboard?.kind !== "table-copy") return [];
|
|
return clipboard.tables.map((entry) => ({
|
|
...entry,
|
|
schema: normalizeTreeClipboardSchema(entry.connectionId, entry.database, entry.schema),
|
|
}));
|
|
}
|
|
|
|
function canPasteTreeClipboardToCurrentNode(): boolean {
|
|
return currentDatabaseType() !== "victoriametrics" && tableClipboardMatchesTarget(normalizedTreeClipboardTableEntries(), pasteTableTargetContext());
|
|
}
|
|
|
|
function canTransferTreeClipboardToCurrentNode(): boolean {
|
|
const entries = normalizedTreeClipboardTableEntries();
|
|
const target = pasteTableTargetContext();
|
|
if (!target || entries.length === 0) return false;
|
|
const source = tableClipboardSourceContext(entries);
|
|
const sourceConfig = source ? connectionStore.getConfig(source.connectionId) : undefined;
|
|
const targetConfig = connectionStore.getConfig(target.connectionId);
|
|
return !!source && !!sourceConfig && !!targetConfig && supportsTransfer(sourceConfig.db_type) && supportsTransfer(targetConfig.db_type) && !connectionIsEffectivelyReadOnly(targetConfig) && !tableClipboardMatchesTarget(entries, target);
|
|
}
|
|
|
|
function openTransferFromTreeClipboard(): boolean {
|
|
const clipboard = connectionStore.treeClipboard;
|
|
const entries = normalizedTreeClipboardTableEntries();
|
|
const target = pasteTableTargetContext();
|
|
if (clipboard?.kind !== "table-copy" || !target) return false;
|
|
const source = tableClipboardSourceContext(entries);
|
|
if (!source || tableClipboardMatchesTarget(entries, target)) return false;
|
|
connectionStore.transferSource = {
|
|
connectionId: source.connectionId,
|
|
database: source.database,
|
|
schema: source.schema ?? undefined,
|
|
tables: clipboard.tables.map((entry) => entry.tableName),
|
|
targetConnectionId: target.connectionId,
|
|
targetDatabase: target.database,
|
|
targetSchema: target.schema ?? undefined,
|
|
};
|
|
return true;
|
|
}
|
|
|
|
function requestPasteTreeClipboard(): boolean {
|
|
claimTreeItemDialogOwnership();
|
|
ensureDangerDialogRouting();
|
|
routeTreeItemDialogController();
|
|
const clipboard = connectionStore.treeClipboard;
|
|
if (clipboard?.kind === "saved-sql-copy") {
|
|
const target = savedSqlPasteTargetForNode(activeNode.value);
|
|
if (!target || clipboard.fileIds.length === 0) return false;
|
|
void savedSqlStore
|
|
.copyFilesToDatabase(clipboard.fileIds, target)
|
|
.then((files) => {
|
|
if (files.length > 0) toast(t("savedSql.pasted", { count: files.length }), 2000);
|
|
else toast(t("savedSql.nothingToPaste"), 3000);
|
|
})
|
|
.catch((e: unknown) => toast(t("savedSql.pasteFailed", { message: savedSqlErrorMessage(e, t) }), 5000));
|
|
return true;
|
|
}
|
|
if (currentDatabaseType() === "victoriametrics") return false;
|
|
if (clipboard?.kind === "connection-copy") {
|
|
const targetGroupId = connectionPasteTargetGroupId(activeNode.value, (connectionId) => connectionStore.groupIdForConnection(connectionId));
|
|
void connectionStore
|
|
.pasteConnectionClipboard(targetGroupId)
|
|
.then((count) => {
|
|
if (count > 0) toast(count > 1 ? t("connection.duplicatedSelected", { count }) : t("connection.duplicated"), 2000);
|
|
})
|
|
.catch((e: any) => toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000));
|
|
return true;
|
|
}
|
|
if (clipboard?.kind !== "table-copy") return false;
|
|
if (canTransferTreeClipboardToCurrentNode()) return openTransferFromTreeClipboard();
|
|
if (!canPasteTreeClipboardToCurrentNode()) return false;
|
|
pasteTableMode.value = defaultPasteTableMode(currentDatabaseType());
|
|
pasteTableEntries.value = clipboard.tables.map((entry) => ({
|
|
sourceName: entry.tableName,
|
|
targetName: `${entry.tableName}_copy`,
|
|
connectionId: entry.connectionId,
|
|
database: entry.database,
|
|
schema: normalizeTreeClipboardSchema(entry.connectionId, entry.database, entry.schema),
|
|
tableComment: entry.tableComment,
|
|
}));
|
|
showPasteDialog.value = true;
|
|
return true;
|
|
}
|
|
|
|
function requestRefreshSelectedNode(): boolean {
|
|
if (!canRefreshTreeNodeShortcut()) return false;
|
|
void refresh();
|
|
return true;
|
|
}
|
|
|
|
function canRefreshTreeNodeShortcut(): boolean {
|
|
const type = activeNode.value.type;
|
|
if (type === "connection" || type === "database" || type === "schema" || type === "table" || type === "view") {
|
|
return true;
|
|
}
|
|
return isGroupLabel(activeNode.value) && type !== "group-partitions";
|
|
}
|
|
|
|
function requestRenameSelectedNode(): boolean {
|
|
const selected = selectedTreeNodesInVisibleOrder();
|
|
if (selected.length > 1 && selected.some((node) => node.id === activeNode.value.id)) return false;
|
|
const editTarget = selectedConnectionEditTarget(activeNode.value, selected);
|
|
if (editTarget) {
|
|
emit("request-connection-rename", editTarget.connectionId);
|
|
return true;
|
|
}
|
|
if (canRenameMongoCollection.value) {
|
|
openRenameMongoCollectionDialog();
|
|
return true;
|
|
}
|
|
if (canRenameDatabase.value) {
|
|
openRenameDatabaseDialog();
|
|
return true;
|
|
}
|
|
if (canRenameObject.value) {
|
|
openRenameObjectDialog();
|
|
return true;
|
|
}
|
|
if (activeNode.value.type === "connection-group") {
|
|
startRenameGroup();
|
|
return true;
|
|
}
|
|
if (activeNode.value.type === "table-vgroup" && activeNode.value.vgroupId) {
|
|
emit("request-group-rename", activeNode.value.id);
|
|
return true;
|
|
}
|
|
if (activeNode.value.type === "saved-sql-file" && activeNode.value.savedSqlId) {
|
|
emit("request-saved-sql-rename", activeNode.value.id);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function openRenameMongoCollectionDialog() {
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
prepareRenameMongoCollectionDialog();
|
|
}
|
|
|
|
function openCloneMongoCollectionDialog() {
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
prepareCloneMongoCollectionDialog();
|
|
}
|
|
|
|
function openCreateMongoIndexDialog() {
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
prepareCreateMongoIndexDialog();
|
|
}
|
|
|
|
function openCreateMeilisearchIndexDialog() {
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
prepareCreateMeilisearchIndexDialog();
|
|
}
|
|
|
|
function openMongoIndexManagerDialog() {
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
prepareMongoIndexManagerDialog();
|
|
}
|
|
|
|
function openRedisDatabaseAliasDialog() {
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
prepareRedisDatabaseAliasDialog();
|
|
}
|
|
|
|
function requestEditSelectedConnection(): boolean {
|
|
const editTarget = selectedConnectionEditTarget(activeNode.value, selectedTreeNodesInVisibleOrder());
|
|
if (!editTarget) return false;
|
|
connectionStore.startEditing(editTarget.connectionId);
|
|
return true;
|
|
}
|
|
|
|
function requestDeleteSelectedNode(): boolean {
|
|
claimTreeItemDialogOwnership();
|
|
ensureDangerDialogRouting();
|
|
routeTreeItemDialogController();
|
|
if (activeNode.value.type === "saved-sql-file" && activeNode.value.savedSqlId) {
|
|
showDeleteSavedSqlConfirm.value = true;
|
|
return true;
|
|
}
|
|
if (requestDropSelectedNodes()) return true;
|
|
if (activeNode.value.type === "connection") {
|
|
deleteConnection();
|
|
return true;
|
|
}
|
|
if (activeNode.value.type === "connection-group") {
|
|
deleteConnectionGroup();
|
|
return true;
|
|
}
|
|
if (activeNode.value.type === "table-vgroup") {
|
|
requestTableVGroupDelete(activeNode.value);
|
|
return true;
|
|
}
|
|
if (canDropDatabase.value) {
|
|
dropDatabase();
|
|
return true;
|
|
}
|
|
if (canDropMongoDatabase.value) {
|
|
dropDatabase();
|
|
return true;
|
|
}
|
|
if (canDropMilvusDatabase.value) {
|
|
dropDatabase();
|
|
return true;
|
|
}
|
|
if (canDropMongoCollection.value) {
|
|
dropMongoCollection();
|
|
return true;
|
|
}
|
|
if (canDropMilvusCollection.value) {
|
|
dropMilvusCollection();
|
|
return true;
|
|
}
|
|
if (canDropSchema.value) {
|
|
dropSchema();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function onDoubleClick(event: MouseEvent) {
|
|
if (dataTabOpenModeFromTreeClick(activeNode.value.type, event, settingsStore.editorSettings.shortcuts.openDataInNewTab) === "new-tab") return;
|
|
if (activeNode.value.type === "event") {
|
|
void openObjectBrowser(false, true);
|
|
return;
|
|
}
|
|
if (activeNode.value.type === "group-events") {
|
|
void openObjectBrowser();
|
|
return;
|
|
}
|
|
const action = treeNodeRowDoubleClickAction(activeNode.value.type, canOpenObjectBrowser.value, settingsStore.editorSettings.sidebarActivation, canExpand.value, currentDatabaseType(), canOpenConnectionDatabaseBrowser.value, settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation);
|
|
if (action === "open-database-browser") {
|
|
void openDatabaseBrowser();
|
|
} else if (action === "open-object-browser") {
|
|
void openObjectBrowser(false, false, true);
|
|
} else if (action === "open-object-browser-and-expand") {
|
|
void openObjectBrowser(false, false, true);
|
|
if (!activeNode.value.isExpanded) void toggle();
|
|
} else if (action === "open-data") {
|
|
openDataImmediately(activeNode.value);
|
|
} else if (action === "activate-data") {
|
|
activateDataTableFromDoubleClick();
|
|
} else if (action === "open-source") {
|
|
openObjectSourceDialog(false);
|
|
} else if (action === "open-extension-details") {
|
|
emit("open-extension-details", activeNode.value);
|
|
} else if (action === "open-saved-sql") {
|
|
openSavedSqlFile();
|
|
} else if (action === "toggle" && (activeNode.value.type === "mongo-gridfs" || isDocumentBrowserTreeNode(activeNode.value.type))) {
|
|
openMongoTreeData(activeNode.value);
|
|
} else if (action === "toggle") {
|
|
toggle();
|
|
}
|
|
}
|
|
|
|
function activateDataTableFromDoubleClick() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "table" || !hasNodeDatabaseContext(node)) return;
|
|
const activation = settingsStore.editorSettings.sidebarActivation;
|
|
const existingSameTableTab = findExistingSameTableDataTab();
|
|
const action = dataTableDoubleClickAction(existingSameTableTab, activation, settingsStore.editorSettings.dataTabReuseMode);
|
|
if (action === "none") return;
|
|
if (action === "open") {
|
|
openDataImmediately(node);
|
|
return;
|
|
}
|
|
if (!existingSameTableTab) return;
|
|
// Reopening an available table follows DBeaver's editor reuse model: only
|
|
// activate the existing tab so filters, result rows, and in-flight work stay intact.
|
|
queryStore.switchTab(existingSameTableTab.id);
|
|
}
|
|
|
|
function findExistingSameTableDataTab() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "table" || !hasNodeDatabaseContext(node)) return undefined;
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
|
|
return queryStore.tabs.find((tab) => tab.mode === "data" && tab.connectionId === node.connectionId && tab.database === node.database && (tab.tableMeta?.catalog || "") === (node.catalog || "") && (tab.schema || "") === (tableSchema || "") && (tab.tableMeta?.tableName || tab.title) === node.label);
|
|
}
|
|
|
|
function openMongoTreeData(node: TreeNode) {
|
|
if (!node.connectionId || !node.database) return;
|
|
if (node.type === "mongo-gridfs") {
|
|
queryStore.openMongoGridFs(node.connectionId, node.database);
|
|
return;
|
|
}
|
|
const tabTitle = `${node.database}.${node.label}`;
|
|
if (node.type === "mongo-bucket") {
|
|
queryStore.openMongoBucket(node.connectionId, node.database, node.label);
|
|
return;
|
|
}
|
|
if (node.type === "dynamodb-table") {
|
|
const tab = queryStore.createTab(node.connectionId, node.database, `${node.database}.${node.label}`, "mongo");
|
|
queryStore.updateSql(tab, node.label);
|
|
queryStore.setTableMeta(tab, {
|
|
database: node.database,
|
|
tableName: node.label,
|
|
tableType: "DYNAMODB TABLE",
|
|
columns: [],
|
|
primaryKeys: [],
|
|
});
|
|
return;
|
|
}
|
|
if (node.type !== "mongo-collection") return;
|
|
const existing = queryStore.tabs.find((tab) => tab.mode === "mongo" && tab.connectionId === node.connectionId && tab.database === node.database && tab.tableMeta?.tableName === node.label);
|
|
if (existing) {
|
|
queryStore.switchTab(existing.id);
|
|
return;
|
|
}
|
|
const tab = queryStore.createTab(node.connectionId, node.database, tabTitle, "mongo");
|
|
queryStore.updateSql(tab, node.label);
|
|
queryStore.setTableMeta(tab, {
|
|
database: node.database,
|
|
tableName: node.label,
|
|
tableType: mongoCollectionTableTypeFromNode(node),
|
|
columns: [],
|
|
primaryKeys: [],
|
|
});
|
|
}
|
|
|
|
async function openSavedSqlFile() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "saved-sql-file" || !node.savedSqlId) return;
|
|
const file = await savedSqlStore.ensureFileContent(node.savedSqlId);
|
|
if (!file) return;
|
|
const tabId = queryStore.openSavedSql(file);
|
|
connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId;
|
|
void savedSqlStore.recordFileUsage(file.id);
|
|
}
|
|
|
|
async function copySavedSqlFiles() {
|
|
const selected = selectedTreeNodesInVisibleOrder();
|
|
const nodes = selected.length > 1 && selected.some((node) => node.id === activeNode.value.id) ? selected : [activeNode.value];
|
|
const fileIds = savedSqlClipboardFileIds(nodes);
|
|
if (fileIds.length === 0) return;
|
|
connectionStore.treeClipboard = { kind: "saved-sql-copy", fileIds };
|
|
try {
|
|
await copyToClipboard(
|
|
nodes
|
|
.filter((node) => node.type === "saved-sql-file" && !!node.savedSqlId)
|
|
.map(copyNameForTreeNode)
|
|
.join("\n"),
|
|
);
|
|
} catch {
|
|
/* Internal SQL clipboard remains usable when the system clipboard is unavailable. */
|
|
}
|
|
toast(t("savedSql.copied", { count: fileIds.length }), 2000);
|
|
}
|
|
|
|
async function exportSavedSqlFile() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "saved-sql-file" || !node.savedSqlId) return;
|
|
try {
|
|
const file = await savedSqlStore.ensureFileContent(node.savedSqlId);
|
|
if (!file) return;
|
|
const result = await exportSavedSqlFileContent(file.sql, file.name);
|
|
if (result === "saved") toast(t("sqlLibrary.exported"), 2000);
|
|
} catch (e: any) {
|
|
toast(t("sqlLibrary.exportFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
function renameSavedSqlFile() {
|
|
const node = activeNode.value;
|
|
if (node.type === "saved-sql-file" && node.savedSqlId) emit("request-saved-sql-rename", node.id);
|
|
}
|
|
|
|
function deleteSavedSqlFile() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "saved-sql-file" || !node.savedSqlId) return;
|
|
claimTreeItemDialogOwnership();
|
|
ensureDangerDialogRouting();
|
|
showDeleteSavedSqlConfirm.value = true;
|
|
}
|
|
|
|
async function confirmDeleteSavedSqlFile() {
|
|
const node = sidebarDangerTarget.value ?? activeNode.value;
|
|
if (node.type !== "saved-sql-file" || !node.savedSqlId) return;
|
|
await savedSqlStore.deleteFile(node.savedSqlId);
|
|
connectionStore.removeTreeNode(node.id);
|
|
releaseActiveNodeReference([node.id]);
|
|
}
|
|
|
|
async function openObjectBrowser(eventReadOnly = false, openEventEditor: boolean | "create" = false, focusSearch = false) {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
|
|
const eventName = openEventEditor === true && node.type === "event" ? node.objectName || node.label : undefined;
|
|
const eventCreateRequestId = openEventEditor === "create" && node.type === "group-events" ? ++mysqlEventCreateRequestSeq : undefined;
|
|
const objectFilter = node.type === "event" || node.type === "group-events" ? "events" : undefined;
|
|
|
|
const connection = connectionStore.getConfig(node.connectionId);
|
|
if (!connection) return;
|
|
if (connection.db_type === "plugin") {
|
|
await queryStore.openPluginConnection(node.connectionId);
|
|
return;
|
|
}
|
|
if (hasTreeNodeDatabaseContext(node)) {
|
|
const tabId = queryStore.openObjectBrowser(node.connectionId, node.database, node.schema, node.catalog, eventName, eventReadOnly, objectFilter, eventCreateRequestId);
|
|
if (focusSearch) await nextTick(() => requestObjectBrowserSearchFocus(tabId));
|
|
return;
|
|
}
|
|
const options = await getDatabaseOptions(node.connectionId);
|
|
const database = resolveDefaultDatabase(connection, options);
|
|
if (database) {
|
|
const tabId = queryStore.openObjectBrowser(node.connectionId, database, undefined, undefined, eventName, eventReadOnly, objectFilter, eventCreateRequestId);
|
|
if (focusSearch) await nextTick(() => requestObjectBrowserSearchFocus(tabId));
|
|
} else {
|
|
await toggle();
|
|
}
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
openDriverStoreForInstallError(e?.message || String(e));
|
|
}
|
|
}
|
|
|
|
// 每次"新建事件"菜单点击都会分配一个单调递增的请求号,保证同一个
|
|
// ObjectBrowser tab 被复用时也能重新进入 MySQL Event 的 CREATE 编辑器。
|
|
let mysqlEventCreateRequestSeq = 0;
|
|
|
|
function openMysqlEventCreateEditor() {
|
|
void openObjectBrowser(false, "create");
|
|
}
|
|
|
|
async function openDatabaseBrowser() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "connection" || !node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
queryStore.openDatabaseBrowser(node.connectionId);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
openDriverStoreForInstallError(e?.message || String(e));
|
|
}
|
|
}
|
|
|
|
async function openProfileDatabaseWorkspace() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const workspace = driverProfileDatabaseWorkspace(config?.driver_profile);
|
|
if (!workspace?.entryScopes.includes("database") || node.type !== "database" || !node.database) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
const target = workspace.resolveTarget?.(node.database, "database") ?? { database: node.database };
|
|
queryStore.openDriverProfileWorkspace(node.connectionId, target.database, t(workspace.tabTitleKey), workspace.mode, workspace.tabScope, target.branch);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openProfileConnectionWorkspace() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "connection" || !node.connectionId) return;
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const workspace = driverProfileDatabaseWorkspace(config?.driver_profile);
|
|
if (!workspace?.entryScopes.includes("connection")) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
const options = await getDatabaseOptions(node.connectionId);
|
|
const database = resolveDefaultDatabase(config!, options);
|
|
if (!database) return;
|
|
const target = workspace.resolveTarget?.(database, "connection") ?? { database };
|
|
queryStore.openDriverProfileWorkspace(node.connectionId, target.database, t(workspace.tabTitleKey), workspace.mode, workspace.tabScope, target.branch);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openUserAdmin() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
queryStore.openUserAdmin(node.connectionId);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openProcessList() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
queryStore.openProcessList(node.connectionId);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openSqlServerActivityTrace() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
queryStore.openSqlServerActivityTrace(node.connectionId);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openServerDashboard() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
if (currentDatabaseType() === "nacos") {
|
|
queryStore.openNacosDashboard(node.connectionId);
|
|
} else if (connectionSupportsXuguServerDashboard(connectionStore.getConfig(node.connectionId))) {
|
|
queryStore.openXuguDashboard(node.connectionId);
|
|
} else if (connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId))) {
|
|
queryStore.openPostgresDashboard(node.connectionId);
|
|
} else {
|
|
queryStore.openMysqlDashboard(node.connectionId);
|
|
}
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openDamengUsers() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
queryStore.openDamengUsers(node.connectionId);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openDamengRoles() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
queryStore.openDamengRoles(node.connectionId);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openDamengJobAdmin() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
queryStore.openDamengJobAdmin(node.connectionId);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
function scheduleOpenData(node: TreeNode) {
|
|
emit("open-data", node, true, "default", openData);
|
|
}
|
|
|
|
function openDataImmediately(node: TreeNode = activeNode.value) {
|
|
emit("open-data", node, false, "default", openData);
|
|
}
|
|
|
|
function openDataInNewTabImmediately(node: TreeNode = activeNode.value) {
|
|
emit("open-data", node, false, "new-tab", (target, request) => openData(target, request, "new-tab"));
|
|
}
|
|
|
|
async function newQuery() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
if (hasTreeNodeDatabaseContext(node)) {
|
|
if (node.type === "table" || node.type === "view" || node.type === "materialized_view") {
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const dbType = config ? effectiveDatabaseTypeForConnection(config) : undefined;
|
|
const identifierQuote = connectionStore.connectionIdentifierQuote(node.connectionId);
|
|
const sql = buildTableSelectTemplate({
|
|
databaseType: dbType,
|
|
identifierQuote,
|
|
catalog: node.catalog,
|
|
database: node.database,
|
|
schema: node.schema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: node.label,
|
|
columns: [],
|
|
});
|
|
openSqlTemplateTab(node.connectionId, node.database, node.schema, node.catalog, sql);
|
|
return;
|
|
}
|
|
queryStore.createTab(node.connectionId, node.database, undefined, "query", node.schema, undefined, node.catalog);
|
|
return;
|
|
}
|
|
const connection = connectionStore.getConfig(node.connectionId);
|
|
if (!connection) return;
|
|
const options = await getDatabaseOptions(node.connectionId);
|
|
queryStore.createTab(node.connectionId, resolveDefaultDatabase(connection, options), undefined, "query", connection.default_schema);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
openDriverStoreForInstallError(e?.message || String(e));
|
|
}
|
|
}
|
|
|
|
// SQL template helpers have been extracted to @/lib/tableSqlTemplates.ts
|
|
// ---- Template actions ----
|
|
|
|
function openRedisInstanceInfo() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const dbName = config?.name || "Redis";
|
|
queryStore.createTab(node.connectionId, "0", `${dbName} - ${t("contextMenu.instanceInfo")}`, "redis-dashboard");
|
|
}
|
|
|
|
async function loadTemplateContext(allowView = false, node: TreeNode = activeNode.value) {
|
|
if (!node.connectionId || !hasTreeNodeDatabaseContext(node)) return null;
|
|
const isTableNode = node.type === "table";
|
|
const isReadableObject = isTableNode || (allowView && (node.type === "view" || node.type === "materialized_view"));
|
|
if (!isReadableObject) return null;
|
|
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const dbType = config ? effectiveDatabaseTypeForConnection(config) : undefined;
|
|
const tableSchema = node.schema || node.database;
|
|
let columns: ColumnInfo[] = [];
|
|
try {
|
|
const querySchema = connectionObjectTreeQuerySchema(config, node.database, tableSchema);
|
|
columns = await api.getColumns(node.connectionId, node.database, querySchema, node.label, node.catalog);
|
|
} catch (e) {
|
|
console.warn("[DBX][tableSqlTemplate:getColumns:error]", e);
|
|
}
|
|
|
|
let tableType = node.tableType;
|
|
if (dbType === "tdengine") {
|
|
try {
|
|
const querySchema = connectionObjectTreeQuerySchema(config, node.database, tableSchema);
|
|
const tables = await api.listTables(node.connectionId, node.database, querySchema, node.label, 200, undefined, undefined, node.catalog);
|
|
const matched = tables.find((table) => table.name.toLowerCase() === node.label.toLowerCase());
|
|
if (matched?.table_type) tableType = matched.table_type;
|
|
} catch (e) {
|
|
console.warn("[DBX][tableSqlTemplate:listTables:error]", e);
|
|
}
|
|
}
|
|
|
|
const identifierQuote = connectionStore.connectionIdentifierQuote(node.connectionId);
|
|
return { node, dbType, driverProfile: config?.driver_profile, identifierQuote, tableSchema, columns, tableType };
|
|
}
|
|
|
|
async function openMultiTableSqlTemplate(targets: Array<TreeNode & { connectionId: string; database: string }>, buildSql: (context: NonNullable<Awaited<ReturnType<typeof loadTemplateContext>>>) => string, allowView: boolean, titlePrefix: string) {
|
|
const parts: string[] = [];
|
|
const tabTarget = targets.find((target) => target.id === activeNode.value.id) ?? targets[0]!;
|
|
for (const target of targets) {
|
|
const context = await loadTemplateContext(allowView, target);
|
|
if (!context) continue;
|
|
parts.push(buildSql(context));
|
|
}
|
|
if (!parts.length) return;
|
|
const sql = parts.length === 1 ? parts[0]! : joinExportedDdls(parts);
|
|
const title = targets.length === 1 ? undefined : `${titlePrefix} - ${targets.map((target) => target.label).join(", ")}`;
|
|
openSqlTemplateTab(tabTarget.connectionId, tabTarget.database, tabTarget.schema, tabTarget.catalog, sql, title);
|
|
}
|
|
|
|
function openSqlTemplateTab(connectionId: string, database: string, schema: string | undefined, catalog: string | undefined, sql: string, title?: string) {
|
|
const tabId = queryStore.createTab(connectionId, database, title, "query", schema, undefined, catalog, { forceWordWrap: true });
|
|
queryStore.updateSql(tabId, sql);
|
|
}
|
|
|
|
async function newSelectTemplate() {
|
|
try {
|
|
const targets = selectedDdlTargets().filter((target) => target.type === "table" || target.type === "view" || target.type === "materialized_view");
|
|
if (targets.length > 1) {
|
|
await openMultiTableSqlTemplate(
|
|
targets,
|
|
(context) =>
|
|
buildTableSelectTemplate({
|
|
databaseType: context.dbType,
|
|
driverProfile: context.driverProfile,
|
|
identifierQuote: context.identifierQuote,
|
|
catalog: context.node.catalog,
|
|
database: context.node.database,
|
|
schema: context.tableSchema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: context.node.label,
|
|
columns: context.columns,
|
|
}),
|
|
true,
|
|
"SELECT",
|
|
);
|
|
return;
|
|
}
|
|
const context = await loadTemplateContext(true);
|
|
if (!context) return;
|
|
const sql = buildTableSelectTemplate({
|
|
databaseType: context.dbType,
|
|
driverProfile: context.driverProfile,
|
|
identifierQuote: context.identifierQuote,
|
|
catalog: context.node.catalog,
|
|
database: context.node.database,
|
|
schema: context.tableSchema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: context.node.label,
|
|
columns: context.columns,
|
|
});
|
|
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, context.node.catalog, sql);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function newInsertTemplate() {
|
|
try {
|
|
const targets = selectedDdlTargets().filter((target) => target.type === "table");
|
|
if (targets.length > 1) {
|
|
await openMultiTableSqlTemplate(
|
|
targets,
|
|
(context) =>
|
|
buildTableInsertTemplate({
|
|
databaseType: context.dbType,
|
|
driverProfile: context.driverProfile,
|
|
identifierQuote: context.identifierQuote,
|
|
catalog: context.node.catalog,
|
|
database: context.node.database,
|
|
schema: context.tableSchema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: context.node.label,
|
|
columns: context.columns,
|
|
tableType: context.tableType,
|
|
}),
|
|
false,
|
|
"INSERT",
|
|
);
|
|
return;
|
|
}
|
|
const context = await loadTemplateContext(false);
|
|
if (!context) return;
|
|
const sql = buildTableInsertTemplate({
|
|
databaseType: context.dbType,
|
|
driverProfile: context.driverProfile,
|
|
identifierQuote: context.identifierQuote,
|
|
catalog: context.node.catalog,
|
|
database: context.node.database,
|
|
schema: context.tableSchema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: context.node.label,
|
|
columns: context.columns,
|
|
tableType: context.tableType,
|
|
});
|
|
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, context.node.catalog, sql);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function newUpdateTemplate() {
|
|
try {
|
|
const targets = selectedDdlTargets().filter((target) => target.type === "table");
|
|
if (targets.length > 1) {
|
|
await openMultiTableSqlTemplate(
|
|
targets,
|
|
(context) =>
|
|
buildTableUpdateTemplate({
|
|
databaseType: context.dbType,
|
|
driverProfile: context.driverProfile,
|
|
identifierQuote: context.identifierQuote,
|
|
catalog: context.node.catalog,
|
|
database: context.node.database,
|
|
schema: context.tableSchema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: context.node.label,
|
|
columns: context.columns,
|
|
}),
|
|
false,
|
|
"UPDATE",
|
|
);
|
|
return;
|
|
}
|
|
const context = await loadTemplateContext(false);
|
|
if (!context) return;
|
|
const sql = buildTableUpdateTemplate({
|
|
databaseType: context.dbType,
|
|
driverProfile: context.driverProfile,
|
|
identifierQuote: context.identifierQuote,
|
|
catalog: context.node.catalog,
|
|
database: context.node.database,
|
|
schema: context.tableSchema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: context.node.label,
|
|
columns: context.columns,
|
|
});
|
|
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, context.node.catalog, sql);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function newDeleteTemplate() {
|
|
try {
|
|
const targets = selectedDdlTargets().filter((target) => target.type === "table");
|
|
if (targets.length > 1) {
|
|
await openMultiTableSqlTemplate(
|
|
targets,
|
|
(context) =>
|
|
buildTableDeleteTemplate({
|
|
databaseType: context.dbType,
|
|
driverProfile: context.driverProfile,
|
|
identifierQuote: context.identifierQuote,
|
|
catalog: context.node.catalog,
|
|
database: context.node.database,
|
|
schema: context.tableSchema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: context.node.label,
|
|
columns: context.columns,
|
|
}),
|
|
false,
|
|
"DELETE",
|
|
);
|
|
return;
|
|
}
|
|
const context = await loadTemplateContext(false);
|
|
if (!context) return;
|
|
const sql = buildTableDeleteTemplate({
|
|
databaseType: context.dbType,
|
|
driverProfile: context.driverProfile,
|
|
identifierQuote: context.identifierQuote,
|
|
catalog: context.node.catalog,
|
|
database: context.node.database,
|
|
schema: context.tableSchema,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
tableName: context.node.label,
|
|
columns: context.columns,
|
|
});
|
|
openSqlTemplateTab(context.node.connectionId!, context.node.database!, context.node.schema, context.node.catalog, sql);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function openSidebarMultiTableDdlTab(targets: Array<TreeNode & { connectionId: string; database: string }>) {
|
|
const tabTarget = targets.find((target) => target.id === activeNode.value.id) ?? targets[0]!;
|
|
const sql = await buildSidebarDdlTemplateSql(
|
|
targets,
|
|
async (target) => {
|
|
await connectionStore.ensureConnected(target.connectionId);
|
|
const schema = target.schema || target.database;
|
|
if (target.type === "table") return api.getTableDisplayDdl(target.connectionId, target.database, schema, target.label, undefined, target.catalog);
|
|
if (target.type === "materialized_view") return api.getTableDisplayDdl(target.connectionId, target.database, schema, target.label, "MATERIALIZED_VIEW", target.catalog);
|
|
const result = await api.getObjectSource(target.connectionId, target.database, schema, target.label, "VIEW");
|
|
return buildViewDdl({
|
|
databaseType: databaseTypeForNode(target),
|
|
identifierQuote: connectionStore.connectionIdentifierQuote?.(target.connectionId),
|
|
schema,
|
|
name: target.label,
|
|
source: result.source,
|
|
});
|
|
},
|
|
async (ddl, target) => {
|
|
const formatDialect = sqlFormatDialectForDbType(databaseTypeForNode(target));
|
|
const formatted = await formatSqlForDisplay(ddl, formatDialect, settingsStore.editorSettings.sqlFormatter);
|
|
return formatSidebarDdlTemplateForDisplay(formatted, formatDialect, databaseTypeForNode(target), settingsStore.editorSettings.generateSqlIncludeDatabaseName, settingsStore.editorSettings.generateSqlQuoteIdentifiers, target.catalog);
|
|
},
|
|
);
|
|
connectionStore.activeConnectionId = tabTarget.connectionId;
|
|
const title = `DDL - ${targets.map((target) => target.label).join(", ")}`;
|
|
openSqlTemplateTab(tabTarget.connectionId, tabTarget.database, tabTarget.schema, tabTarget.catalog, sql, title);
|
|
}
|
|
|
|
async function generateDdlTemplate() {
|
|
if (currentDatabaseType() === "victoriametrics") return;
|
|
const targets = selectedDdlTargets();
|
|
if (!targets.length) return;
|
|
try {
|
|
await openSidebarMultiTableDdlTab(targets);
|
|
} catch (e: any) {
|
|
toast(e?.message || String(e), 5000);
|
|
}
|
|
}
|
|
|
|
function selectedDdlTargets() {
|
|
return resolveSidebarDdlTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds);
|
|
}
|
|
|
|
async function openDdl() {
|
|
if (currentDatabaseType() === "victoriametrics") return;
|
|
const targets = selectedDdlTargets();
|
|
if (!targets.length) return;
|
|
if (targets.length > 1) {
|
|
try {
|
|
await openSidebarMultiTableDdlTab(targets);
|
|
} catch (e: any) {
|
|
toast(e?.message || String(e), 5000);
|
|
}
|
|
return;
|
|
}
|
|
emit("open-ddl", targets[0]!);
|
|
}
|
|
|
|
async function openDdlForSelection(node: TreeNode, selectedNodeIds: readonly string[]): Promise<boolean> {
|
|
if (node.type !== "table" && node.type !== "view" && node.type !== "materialized_view") return false;
|
|
activateRuntimeNode(node);
|
|
const dbType = node.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
|
|
if (dbType === "victoriametrics") return false;
|
|
const targets = resolveSidebarDdlTargets(node, connectionStore.treeNodes, selectedNodeIds);
|
|
if (!targets.length) return false;
|
|
if (targets.length > 1) {
|
|
try {
|
|
await openSidebarMultiTableDdlTab(targets);
|
|
} catch (e: any) {
|
|
toast(e?.message || String(e), 5000);
|
|
}
|
|
return true;
|
|
}
|
|
emit("open-ddl", targets[0]!);
|
|
return true;
|
|
}
|
|
|
|
function openElasticsearchIndexMetadata(kind: ElasticsearchIndexMetadataKind) {
|
|
emit("open-elasticsearch-index-metadata", createSidebarActionTarget(activeNode.value), kind);
|
|
}
|
|
|
|
async function refresh() {
|
|
const node = activeNode.value;
|
|
if (node.type === "connection" && node.connectionId) {
|
|
try {
|
|
await connectionStore.refreshConnectionTreeNode(node);
|
|
} catch (e: any) {
|
|
if (isObjectCacheInvalidationError(e)) {
|
|
toast(t("connection.objectCacheRefreshFailed", { message: translateBackendError(t, e) }), 5000);
|
|
return;
|
|
}
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
openDriverStoreForInstallError(e?.message || String(e), node);
|
|
}
|
|
return;
|
|
}
|
|
try {
|
|
await connectionStore.refreshTreeNode(node);
|
|
} catch (e: any) {
|
|
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
|
openDriverStoreForInstallError(e?.message || String(e), node);
|
|
}
|
|
}
|
|
|
|
async function copyName() {
|
|
try {
|
|
await copyToClipboard(formatSelectedTableNamesForClipboard());
|
|
toast(t("connection.copied"), 2000);
|
|
} catch (e: any) {
|
|
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function copyDisplayPath() {
|
|
const node = activeNode.value;
|
|
const connectionName = node.connectionId ? connectionStore.getConfig(node.connectionId)?.name || "" : "";
|
|
const path = copyDisplayPathForTreeNode(node, connectionName);
|
|
if (!path) return;
|
|
try {
|
|
await copyToClipboard(path);
|
|
toast(t("connection.copied"), 2000);
|
|
} catch (e: any) {
|
|
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
function copyNameMenuItem(): ContextMenuItem {
|
|
const node = activeNode.value;
|
|
const connectionName = node.connectionId ? connectionStore.getConfig(node.connectionId)?.name || "" : "";
|
|
if (currentDatabaseType() === "mysql" && copyDisplayPathForTreeNode(node, connectionName)) {
|
|
return {
|
|
label: t("contextMenu.copyName"),
|
|
icon: Copy,
|
|
children: [
|
|
{ label: t("contextMenu.name"), action: copyName, icon: Copy },
|
|
{ label: t("contextMenu.fullPath"), action: copyDisplayPath, icon: Copy },
|
|
],
|
|
};
|
|
}
|
|
return { label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value };
|
|
}
|
|
|
|
const CONNECTION_URL_COPY_LABEL_KEYS: Record<ConnectionUrlCopyFormat, string> = {
|
|
url: "contextMenu.copyConnectionUrl",
|
|
urlWithPassword: "contextMenu.copyConnectionUrlWithPassword",
|
|
jdbcUrl: "contextMenu.copyJdbcUrl",
|
|
jdbcUrlWithCredentials: "contextMenu.copyJdbcUrlWithCredentials",
|
|
hostPort: "contextMenu.copyHostPort",
|
|
dsn: "contextMenu.copyDsn",
|
|
dsnWithPassword: "contextMenu.copyDsnWithPassword",
|
|
psqlCommand: "contextMenu.copyPsqlCommand",
|
|
};
|
|
|
|
/**
|
|
* "Copy Connection Info" submenu for connection/database nodes: standard URL,
|
|
* JDBC URL, host:port, libpq DSN and psql command, trimmed to what the
|
|
* connection's dialect actually supports. The primary entries never embed the
|
|
* stored password; password-inclusive entries say so in their label and are
|
|
* gated behind a confirmation dialog. `databaseOverride` lets a database node
|
|
* copy the URL for that specific database instead of the default one.
|
|
*/
|
|
function connectionUrlCopyMenuItem(databaseOverride?: string): ContextMenuItem | null {
|
|
const node = activeNode.value;
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
if (!config) return null;
|
|
const formats = connectionUrlCopyFormats(config);
|
|
if (formats.length === 0) return null;
|
|
return {
|
|
label: t("contextMenu.copyConnectionInfo"),
|
|
icon: Link2,
|
|
children: formats.map((format) => ({
|
|
label: t(CONNECTION_URL_COPY_LABEL_KEYS[format]),
|
|
action: () => copyConnectionUrlFormat(config, format, databaseOverride),
|
|
})),
|
|
};
|
|
}
|
|
|
|
const showCopyConnectionSecretConfirm = shallowRef(false);
|
|
let pendingConnectionSecretCopy: { config: ConnectionConfig; format: ConnectionUrlCopyFormat; databaseOverride?: string } | null = null;
|
|
|
|
function copyConnectionUrlFormat(config: ConnectionConfig, format: ConnectionUrlCopyFormat, databaseOverride?: string) {
|
|
if (CONNECTION_URL_COPY_WITH_PASSWORD_FORMATS.has(format)) {
|
|
pendingConnectionSecretCopy = { config, format, databaseOverride };
|
|
showCopyConnectionSecretConfirm.value = true;
|
|
return;
|
|
}
|
|
return performConnectionUrlCopy(config, format, databaseOverride);
|
|
}
|
|
|
|
async function performConnectionUrlCopy(config: ConnectionConfig, format: ConnectionUrlCopyFormat, databaseOverride?: string) {
|
|
const text = buildConnectionUrlCopy(config, format, { database: databaseOverride });
|
|
if (!text) return;
|
|
try {
|
|
await copyToClipboard(text);
|
|
toast(t("connection.copied"), 2000);
|
|
} catch (e: any) {
|
|
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function copyCustomTypeDdl() {
|
|
const node = createSidebarActionTarget(activeNode.value);
|
|
if (!node.connectionId || !node.database) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const details = await api.getCustomTypeDetails(node.connectionId, node.database, node.schema || node.database, node.objectName || node.label);
|
|
const sql = details.ddl?.sql?.trim();
|
|
if (!sql) {
|
|
toast(t("customType.ddl.empty"), 3000);
|
|
return;
|
|
}
|
|
if (!details.ddl?.complete) {
|
|
toast(t("customType.ddl.incomplete"), 5000);
|
|
return;
|
|
}
|
|
await copyToClipboard(sql);
|
|
toast(t("contextMenu.ddlCopied"), 2000);
|
|
} catch (error: any) {
|
|
toast(t("grid.copyFailed", { message: translateBackendError(t, error) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function copySelectedNames() {
|
|
const selectedNodes = selectedTreeNodesInVisibleOrder();
|
|
const nodes = selectedNodes.length > 1 && selectedNodes.some((node) => node.id === activeNode.value.id) ? selectedNodes : [activeNode.value];
|
|
const connectionTargets = selectedConnectionClipboardTargets(activeNode.value, nodes);
|
|
if (connectionTargets.length > 0) {
|
|
const copiedCount = connectionStore.copyConnectionsToTreeClipboard(connectionTargets.map((node) => node.connectionId));
|
|
if (copiedCount > 0) {
|
|
try {
|
|
await copyToClipboard(connectionTargets.map(copyNameForTreeNode).join("\n"));
|
|
} catch {
|
|
/* system clipboard copy is best-effort */
|
|
}
|
|
toast(t("connection.copied"), 2000);
|
|
}
|
|
return;
|
|
}
|
|
const savedSqlFileIds = savedSqlClipboardFileIds(nodes);
|
|
if (savedSqlFileIds.length > 0) {
|
|
connectionStore.treeClipboard = { kind: "saved-sql-copy", fileIds: savedSqlFileIds };
|
|
try {
|
|
await copyToClipboard(
|
|
nodes
|
|
.filter((node) => node.type === "saved-sql-file" && !!node.savedSqlId)
|
|
.map(copyNameForTreeNode)
|
|
.join("\n"),
|
|
);
|
|
} catch {
|
|
/* Internal SQL clipboard remains usable when the system clipboard is unavailable. */
|
|
}
|
|
toast(t("savedSql.copied", { count: savedSqlFileIds.length }), 2000);
|
|
return;
|
|
}
|
|
updateTreeClipboardForNodes(nodes);
|
|
try {
|
|
await copyToClipboard(formatSelectedTableNamesForClipboard(selectedNodes));
|
|
toast(t("connection.copied"), 2000);
|
|
} catch (e: any) {
|
|
toast(t("grid.copyFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
function updateTreeClipboardForNodes(nodes: TreeNode[]) {
|
|
const tableNodes = nodes.filter((node): node is DuplicateStructureSource => node.type === "table" && !!node.connectionId && !!node.database && typeof node.label === "string");
|
|
if (tableNodes.length === 0) {
|
|
connectionStore.treeClipboard = null;
|
|
return;
|
|
}
|
|
if (tableNodes.some((node) => databaseTypeForNode(node) === "victoriametrics")) {
|
|
connectionStore.treeClipboard = null;
|
|
return;
|
|
}
|
|
connectionStore.treeClipboard = {
|
|
kind: "table-copy",
|
|
tables: tableNodes.map((node) => ({
|
|
connectionId: node.connectionId,
|
|
database: node.database,
|
|
schema: normalizeTreeClipboardSchema(node.connectionId, node.database, node.schema),
|
|
tableName: node.label,
|
|
tableComment: node.comment,
|
|
})),
|
|
};
|
|
}
|
|
|
|
// --- Table Management Operations ---
|
|
const pasteTableDataCopySupported = computed(() => supportsWholeRowTableDataCopy(currentDatabaseType()));
|
|
|
|
// --- Extension Management ---
|
|
function openInstallExtensionDialog(node: TreeNode) {
|
|
emit("open-install-extension", node);
|
|
}
|
|
|
|
// --- Procedure / Function Management ---
|
|
function dropObjectSqlOptions(): DropObjectSqlOptions | null {
|
|
return dropObjectSqlOptionsForNode(activeNode.value);
|
|
}
|
|
|
|
function dropObjectSqlOptionsForNode(node: TreeNode): DropObjectSqlOptions | null {
|
|
if (node.type !== "view" && node.type !== "materialized_view" && node.type !== "procedure" && node.type !== "function" && node.type !== "event") return null;
|
|
return {
|
|
databaseType: tableStructureDatabaseTypeForNode(node),
|
|
objectType: node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : node.type === "procedure" ? "PROCEDURE" : node.type === "function" ? "FUNCTION" : "EVENT",
|
|
schema: node.schema,
|
|
name: node.objectName || node.label,
|
|
signature: node.signature,
|
|
// Cloud Spanner's dialect decides the quote; the static per-type mapping cannot.
|
|
identifierQuote: connectionStore.connectionIdentifierQuote?.(node.connectionId),
|
|
};
|
|
}
|
|
|
|
function tableChildDropObjectType(type: TreeNodeType): TableChildObjectType | null {
|
|
if (type === "column") return "COLUMN";
|
|
if (type === "index") return "INDEX";
|
|
if (type === "fkey") return "FOREIGN_KEY";
|
|
if (type === "trigger") return "TRIGGER";
|
|
return null;
|
|
}
|
|
|
|
function tableChildDropObjectName(node: TreeNode): string {
|
|
if (node.type === "column") return node.meta && "name" in node.meta ? node.meta.name : node.label.replace(/\s+\(.+\)$/, "");
|
|
if (node.type === "index") return node.meta && "name" in node.meta ? node.meta.name : node.label.replace(/\s+\(.+\)$/, "");
|
|
if (node.type === "fkey") return node.meta && "name" in node.meta ? node.meta.name : node.label;
|
|
if (node.type === "trigger") return node.meta && "name" in node.meta ? node.meta.name : node.label.replace(/\s+\(.+\)$/, "");
|
|
return node.label;
|
|
}
|
|
|
|
function dropTableChildObjectSqlOptions(): DropTableChildObjectSqlOptions | null {
|
|
return dropTableChildObjectSqlOptionsForNode(activeNode.value);
|
|
}
|
|
|
|
function dropTableChildObjectSqlOptionsForNode(node: TreeNode): DropTableChildObjectSqlOptions | null {
|
|
const objectType = tableChildDropObjectType(node.type);
|
|
if (!objectType || !node.tableName) return null;
|
|
const name = tableChildDropObjectName(node).trim();
|
|
if (!name) return null;
|
|
return {
|
|
databaseType: databaseTypeForNode(node),
|
|
objectType,
|
|
schema: node.schema,
|
|
tableName: node.tableName,
|
|
name,
|
|
};
|
|
}
|
|
|
|
const canDropTableChildObject = computed(() => {
|
|
return canDropTableChildObjectNode(activeNode.value);
|
|
});
|
|
|
|
function canDropTableChildObjectNode(node: TreeNode): boolean {
|
|
const options = dropTableChildObjectSqlOptionsForNode(node);
|
|
if (!options) return false;
|
|
const capabilities = getTableStructureCapabilities(options.databaseType);
|
|
if (options.objectType === "COLUMN") return capabilities.dropColumn;
|
|
if (options.objectType === "INDEX") return capabilities.dropIndex;
|
|
return true;
|
|
}
|
|
|
|
function dropObjectMenuLabel(): string {
|
|
if (activeNode.value.type === "view") return t("contextMenu.dropView");
|
|
if (activeNode.value.type === "materialized_view") return t("contextMenu.dropView");
|
|
if (activeNode.value.type === "procedure") return t("contextMenu.dropProcedure");
|
|
if (activeNode.value.type === "function") return t("contextMenu.dropFunction");
|
|
if (activeNode.value.type === "event") return t("contextMenu.dropObject");
|
|
return t("contextMenu.dropObject");
|
|
}
|
|
|
|
function dropObjectConfirmTitle(): string {
|
|
if (activeNode.value.type === "view") return t("contextMenu.confirmDropViewTitle");
|
|
if (activeNode.value.type === "materialized_view") return t("contextMenu.confirmDropViewTitle");
|
|
if (activeNode.value.type === "procedure") return t("contextMenu.confirmDropProcedureTitle");
|
|
if (activeNode.value.type === "function") return t("contextMenu.confirmDropFunctionTitle");
|
|
return t("contextMenu.confirmDropObjectTitle");
|
|
}
|
|
|
|
function dropObjectConfirmMessage(): string {
|
|
if (activeNode.value.type === "view") return t("contextMenu.confirmDropViewMessage", { name: activeNode.value.label });
|
|
if (activeNode.value.type === "materialized_view") return t("contextMenu.confirmDropViewMessage", { name: activeNode.value.label });
|
|
if (activeNode.value.type === "procedure") return t("contextMenu.confirmDropProcedureMessage", { name: activeNode.value.label });
|
|
if (activeNode.value.type === "function") return t("contextMenu.confirmDropFunctionMessage", { name: activeNode.value.label });
|
|
return t("contextMenu.confirmDropObjectMessage", { name: activeNode.value.label });
|
|
}
|
|
|
|
function dropTableChildObjectMenuLabel(): string {
|
|
if (activeNode.value.type === "column") return t("contextMenu.dropColumn");
|
|
if (activeNode.value.type === "index") return t("contextMenu.dropIndex");
|
|
if (activeNode.value.type === "fkey") return t("contextMenu.dropForeignKey");
|
|
if (activeNode.value.type === "trigger") return t("contextMenu.dropTrigger");
|
|
return t("contextMenu.dropObject");
|
|
}
|
|
|
|
function dropTableChildObjectConfirmTitle(): string {
|
|
if (activeNode.value.type === "column") return t("contextMenu.confirmDropColumnTitle");
|
|
if (activeNode.value.type === "index") return t("contextMenu.confirmDropIndexTitle");
|
|
if (activeNode.value.type === "fkey") return t("contextMenu.confirmDropForeignKeyTitle");
|
|
if (activeNode.value.type === "trigger") return t("contextMenu.confirmDropTriggerTitle");
|
|
return t("contextMenu.confirmDropObjectTitle");
|
|
}
|
|
|
|
function dropTableChildObjectConfirmMessage(): string {
|
|
return t("contextMenu.confirmDropTableChildObjectMessage", {
|
|
name: tableChildDropObjectName(activeNode.value),
|
|
table: activeNode.value.tableName || "",
|
|
});
|
|
}
|
|
|
|
async function refreshDropObjectPreviewSql() {
|
|
const options = dropObjectSqlOptions();
|
|
dropObjectPreviewSql.value = "";
|
|
dropObjectPreviewSql.value = options ? await buildDropObjectSql(options).catch(() => "") : "";
|
|
}
|
|
|
|
async function refreshDropTableChildObjectPreviewSql() {
|
|
const options = dropTableChildObjectSqlOptions();
|
|
dropTableChildObjectPreviewSql.value = "";
|
|
dropTableChildObjectPreviewSql.value = options ? await buildDropTableChildObjectSql(options).catch(() => "") : "";
|
|
}
|
|
|
|
function openObjectSourceDialog(initialEditing: boolean, viewPackageBody = false) {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId || !node.database) return;
|
|
if (viewPackageBody && (node.type !== "package" || node.xuguPackageBodyAvailable !== true || currentDatabaseType() !== "xugu")) return;
|
|
const sourceNode: TreeNode = viewPackageBody ? { ...node, type: "package-body" } : node;
|
|
// TYPE/TYPE_BODY only have a source implementation on Xugu; PostgreSQL-family
|
|
// connections list user-defined types without a CREATE TYPE getter this cycle.
|
|
if ((sourceNode.type === "type" || sourceNode.type === "type-body") && !supportsTypeObjectSource(currentDatabaseType())) return;
|
|
const connectionId = node.connectionId;
|
|
const database = node.database;
|
|
const sourceTarget = objectSourceTargetForTreeNode(sourceNode);
|
|
if (!sourceTarget) return;
|
|
const schema = sourceTarget.schema || database;
|
|
// issue #9035:两个分支都不再 await 连接与取源。先把容器挂出来(源码 tab /
|
|
// 源码弹窗),ensureConnected 与 getObjectSource 都发生在已挂载的 UI 之内,
|
|
// 加载中有状态、失败就地重试,而不是点击后一段时间毫无反应。
|
|
if (settingsStore.editorSettings.routineSourceOpenMode === "query-tab") {
|
|
queryStore.openObjectSourceTabPending({
|
|
connectionId,
|
|
database,
|
|
title: `Source - ${node.label}`,
|
|
schema,
|
|
catalog: node.catalog,
|
|
initialEditing,
|
|
request: { name: sourceTarget.name, objectType: sourceTarget.objectType, signature: sourceNode.signature },
|
|
});
|
|
return;
|
|
}
|
|
emit("open-object-source", sourceNode, initialEditing);
|
|
}
|
|
|
|
function openProcedureExecution() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "procedure" || !node.connectionId || !node.database) return;
|
|
emit("open-procedure", node);
|
|
}
|
|
|
|
async function openDatabaseObjectDependencies() {
|
|
const node = activeNode.value;
|
|
const provider = databaseDependencyProviderFor(currentDatabaseType());
|
|
if (!provider || !node.connectionId || !node.database || !provider.supports(node)) return;
|
|
const sql = provider.buildQuery(node);
|
|
if (!sql) return;
|
|
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
const objectName = node.objectName || node.label;
|
|
const tabId = queryStore.createTab(node.connectionId, node.database, `${t("contextMenu.viewDependencies")} - ${objectName}`, "query", node.schema, undefined, node.catalog, { forceNew: true });
|
|
queryStore.updateSql(tabId, sql);
|
|
await queryStore.executeTabSql(tabId, sql);
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function compileXuguObject() {
|
|
const node = activeNode.value;
|
|
if (currentDatabaseType() !== "xugu" || !node.connectionId || !node.database) return;
|
|
const sql = buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label });
|
|
if (!sql) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
|
if (!executed) return;
|
|
toast(t("contextMenu.compileObjectSuccess", { name: node.label }), 3000);
|
|
await connectionStore.refreshTreeNode(node);
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function compileDamengView() {
|
|
const node = activeNode.value;
|
|
if (currentDatabaseType() !== "dameng" || node.type !== "view" || !node.connectionId || !node.database) return;
|
|
const sql = buildDamengCompileViewSql({ schema: node.schema, name: node.objectName || node.label });
|
|
if (!sql) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
|
if (!executed) return;
|
|
toast(t("contextMenu.compileObjectSuccess", { name: node.label }), 3000);
|
|
await connectionStore.refreshObjectListTreeNode(node.connectionId, node.database, node.schema);
|
|
window.dispatchEvent(
|
|
new CustomEvent("dbx-refresh-object-browser", {
|
|
detail: {
|
|
connectionId: node.connectionId,
|
|
database: node.database,
|
|
schema: node.schema,
|
|
catalog: node.catalog,
|
|
},
|
|
}),
|
|
);
|
|
} catch (e: any) {
|
|
compileErrorTitle.value = t("contextMenu.compileObjectFailedTitle");
|
|
compileErrorMessage.value = t("contextMenu.compileObjectFailedMessage", { name: node.label, message: e?.message || String(e) });
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
showCompileErrorDialog.value = true;
|
|
}
|
|
}
|
|
|
|
async function executeXuguSchedulerJobAction(action: XuguSchedulerJobAction) {
|
|
const node = activeNode.value;
|
|
if (currentDatabaseType() !== "xugu" || node.type !== "job" || !node.connectionId || !node.database) return;
|
|
const sql = buildXuguSchedulerJobSql(action, node.objectName || node.label);
|
|
if (!sql) return;
|
|
if (action === "drop" && !window.confirm(t("xuguSchedulerJob.confirmDrop", { name: node.label }))) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
|
if (!executed) return;
|
|
toast(t("xuguSchedulerJob.actionSuccess", { action: t(`xuguSchedulerJob.${action}`), name: node.label }), 3000);
|
|
// A job belongs to the synthetic scheduler-job group. `refreshTreeNode`
|
|
// already walks a child back to that group, so this refreshes the list
|
|
// after a destructive action without retaining a stale job node.
|
|
await connectionStore.refreshTreeNode(node);
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
function requestDropObject() {
|
|
void refreshDropObjectPreviewSql();
|
|
showDropObjectConfirm.value = true;
|
|
}
|
|
|
|
function requestDropTableChildObject() {
|
|
if (!canDropTableChildObject.value) return;
|
|
void refreshDropTableChildObjectPreviewSql();
|
|
showDropTableChildObjectConfirm.value = true;
|
|
}
|
|
|
|
function canDropTreeNode(node: TreeNode): boolean {
|
|
if (isSqlServerLinkedNode(node)) return false;
|
|
if (node.type === "table") return !!node.connectionId && !!node.database;
|
|
if (node.type === "view" || node.type === "materialized_view" || node.type === "procedure" || node.type === "function" || node.type === "event") {
|
|
return !!node.connectionId && !!node.database && !!dropObjectSqlOptionsForNode(node);
|
|
}
|
|
if (canDropMongoIndexNode(node)) return true;
|
|
return canDropTableChildObjectNode(node);
|
|
}
|
|
|
|
function droppedTableObjectTypeForNode(node: TreeNode): "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | null {
|
|
if (node.type === "table") return "TABLE";
|
|
if (node.type === "view") return "VIEW";
|
|
if (node.type === "materialized_view") return "MATERIALIZED_VIEW";
|
|
return null;
|
|
}
|
|
|
|
function closeDroppedTableObjectTabsForNode(node: TreeNode) {
|
|
const objectType = droppedTableObjectTypeForNode(node);
|
|
if (!objectType || !node.connectionId || !node.database) return;
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const dataTabSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
|
|
queryStore.closeDroppedTableObjectTabs({
|
|
connectionId: node.connectionId,
|
|
database: node.database,
|
|
schema: dataTabSchema,
|
|
schemaCandidates: [node.schema, dataTabSchema],
|
|
name: node.label,
|
|
objectType,
|
|
});
|
|
}
|
|
|
|
function tableDataRefreshTargetForNode(node: TreeNode) {
|
|
if (!node.connectionId || !node.database) return null;
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const dataTabSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
|
|
return {
|
|
connectionId: node.connectionId,
|
|
database: node.database,
|
|
schema: dataTabSchema,
|
|
schemaCandidates: [node.schema, dataTabSchema],
|
|
catalog: node.catalog,
|
|
name: node.label,
|
|
};
|
|
}
|
|
|
|
async function refreshMutatedTableDataTabsForNode(node: TreeNode) {
|
|
const target = tableDataRefreshTargetForNode(node);
|
|
if (!target) return;
|
|
try {
|
|
await queryStore.refreshDataTabsForTable(target);
|
|
} catch (error) {
|
|
console.warn("[DBX][table-data-refresh-after-mutation:error]", { target, error });
|
|
}
|
|
}
|
|
|
|
async function refreshMutatedTableDataTabsForNodes(nodes: readonly TreeNode[]) {
|
|
for (const target of nodes) {
|
|
await refreshMutatedTableDataTabsForNode(target);
|
|
}
|
|
}
|
|
|
|
function selectedBatchDropTargets(): TreeNode[] {
|
|
return selectedSidebarBatchTargets(activeNode.value, selectedTreeNodesInVisibleOrder(), canDropTreeNode);
|
|
}
|
|
|
|
function selectedBatchTableTargets(): TreeNode[] {
|
|
const targets = selectedBatchDropTargets();
|
|
return targets.length > 1 && targets.every((node) => node.type === "table") ? targets : [];
|
|
}
|
|
|
|
function selectedBatchTruncateTargets(): TreeNode[] {
|
|
const targets = selectedBatchTableTargets();
|
|
return targets.every((node) => supportsTableTruncate(databaseTypeForNode(node))) ? targets : [];
|
|
}
|
|
|
|
function selectedBatchEmptyTargets(): TreeNode[] {
|
|
return selectedBatchTableTargets();
|
|
}
|
|
|
|
function selectedBatchIndexTableName(targets: TreeNode[]): string | null {
|
|
const first = targets[0];
|
|
if (!first) return null;
|
|
const table = first.tableName || first.label;
|
|
return table && targets.every((node) => (node.tableName || node.label) === table) ? table : null;
|
|
}
|
|
|
|
function batchDropMenuLabel(): string {
|
|
const targets = selectedBatchDropTargets();
|
|
if (targets.length > 1 && targets.every((node) => node.type === "index")) {
|
|
return t("contextMenu.batchDropIndexes", { count: targets.length });
|
|
}
|
|
return t("contextMenu.batchDrop", { count: targets.length });
|
|
}
|
|
|
|
function batchDropConfirmTitle(): string {
|
|
const targets = batchDropTargets.value;
|
|
if (targets.length > 1 && targets.every((node) => node.type === "index")) {
|
|
return t("contextMenu.confirmDropIndexTitle");
|
|
}
|
|
return t("contextMenu.confirmBatchDropTitle", { count: targets.length });
|
|
}
|
|
|
|
function batchDropConfirmMessage(): string {
|
|
const targets = batchDropTargets.value;
|
|
const table = selectedBatchIndexTableName(targets);
|
|
if (targets.length > 1 && targets.every((node) => node.type === "index") && table) {
|
|
return t("contextMenu.confirmDropBatchIndexesMessage", { count: targets.length, table });
|
|
}
|
|
return t("contextMenu.confirmBatchDropMessage", { count: targets.length });
|
|
}
|
|
|
|
function batchTruncateMenuLabel(): string {
|
|
return t("contextMenu.batchTruncate", { count: selectedBatchTruncateTargets().length });
|
|
}
|
|
|
|
function batchEmptyMenuLabel(): string {
|
|
return t("contextMenu.batchEmpty", { count: selectedBatchEmptyTargets().length });
|
|
}
|
|
|
|
function batchEmptyConfirmTitle(): string {
|
|
return t("contextMenu.confirmBatchEmptyTitle", { count: batchEmptyTargets.value.length });
|
|
}
|
|
|
|
function batchEmptyConfirmMessage(): string {
|
|
return t("contextMenu.confirmBatchEmptyMessage", { count: batchEmptyTargets.value.length });
|
|
}
|
|
|
|
function batchEmptyConfirmLabel(): string {
|
|
return t("contextMenu.batchEmpty", { count: batchEmptyTargets.value.length });
|
|
}
|
|
|
|
function batchTruncateConfirmTitle(): string {
|
|
return t("contextMenu.confirmBatchTruncateTitle", { count: batchTruncateTargets.value.length });
|
|
}
|
|
|
|
function batchTruncateConfirmMessage(): string {
|
|
return t("contextMenu.confirmBatchTruncateMessage", { count: batchTruncateTargets.value.length });
|
|
}
|
|
|
|
async function dropSqlForTreeNode(node: TreeNode, options?: { cascade?: boolean }): Promise<string | null> {
|
|
if (node.type === "table" && node.connectionId && node.database) {
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
return buildDropTableSql({
|
|
databaseType: databaseTypeForNode(node),
|
|
schema: connectionTableSqlSchema(config, node.schema),
|
|
tableName: node.label,
|
|
cascade: options?.cascade && supportsDropTableCascade(databaseTypeForNode(node)),
|
|
identifierQuote: connectionStore.connectionIdentifierQuote?.(node.connectionId),
|
|
});
|
|
}
|
|
const objectOptions = dropObjectSqlOptionsForNode(node);
|
|
if (objectOptions) return buildDropObjectSql(objectOptions);
|
|
if (canDropMongoIndexNode(node)) {
|
|
return `db.getCollection("${(node.tableName || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}").dropIndex(${JSON.stringify(mongoIndexNameForNode(node))})`;
|
|
}
|
|
const childOptions = dropTableChildObjectSqlOptionsForNode(node);
|
|
if (childOptions && canDropTableChildObjectNode(node)) return buildDropTableChildObjectSql(childOptions);
|
|
return null;
|
|
}
|
|
|
|
async function truncateSqlForTreeNode(node: TreeNode, options?: { cascade?: boolean }): Promise<string | null> {
|
|
if (node.type !== "table" || !node.connectionId || !node.database || !supportsTableTruncate(databaseTypeForNode(node))) return null;
|
|
return buildTruncateTableSql({
|
|
databaseType: databaseTypeForNode(node),
|
|
schema: node.schema,
|
|
tableName: node.label,
|
|
cascade: options?.cascade && supportsTruncateTableCascade(databaseTypeForNode(node)),
|
|
});
|
|
}
|
|
|
|
async function emptySqlForTreeNode(node: TreeNode): Promise<string | null> {
|
|
if (node.type !== "table" || !node.connectionId || !node.database) return null;
|
|
return buildEmptyTableSql({
|
|
databaseType: databaseTypeForNode(node),
|
|
schema: node.schema,
|
|
tableName: node.label,
|
|
});
|
|
}
|
|
|
|
async function refreshBatchDropPreviewSql() {
|
|
const targets = batchDropTargets.value;
|
|
const mongoIndexTargets = targets.filter(canDropMongoIndexNode);
|
|
if (mongoIndexTargets.length) {
|
|
batchDropPreviewSql.value = mongoIndexTargets.map((target) => mongoIndexDropPreview(target, mongoIndexNameForNode(target))).join("\n");
|
|
return;
|
|
}
|
|
const statements: string[] = [];
|
|
const useCascade = canBatchDropCascade.value && batchDropCascade.value;
|
|
for (const target of targets) {
|
|
const sql = await dropSqlForTreeNode(target, { cascade: useCascade });
|
|
if (sql) statements.push(sql);
|
|
}
|
|
batchDropPreviewSql.value = statements.join("\n");
|
|
}
|
|
|
|
async function refreshBatchTruncatePreviewSql() {
|
|
const targets = batchTruncateTargets.value;
|
|
const statements: string[] = [];
|
|
const useCascade = canBatchTruncateCascade.value && batchTruncateCascade.value;
|
|
for (const target of targets) {
|
|
const sql = await truncateSqlForTreeNode(target, { cascade: useCascade });
|
|
if (sql) statements.push(sql);
|
|
}
|
|
batchTruncatePreviewSql.value = statements.join("\n");
|
|
}
|
|
|
|
async function refreshBatchEmptyPreviewSql(targets: TreeNode[]) {
|
|
const statements: string[] = [];
|
|
for (const target of targets) {
|
|
const sql = await emptySqlForTreeNode(target);
|
|
if (sql) statements.push(sql);
|
|
}
|
|
batchEmptyPreviewSql.value = statements.join("\n");
|
|
}
|
|
|
|
function requestBatchDrop() {
|
|
const targets = selectedBatchDropTargets();
|
|
if (!targets.length) return;
|
|
batchDropTargets.value = targets.slice();
|
|
batchDropCascade.value = false;
|
|
batchDropProgress.value = { completed: 0, total: 0 };
|
|
void refreshBatchDropPreviewSql();
|
|
showBatchDropConfirm.value = true;
|
|
}
|
|
|
|
function requestBatchTruncate() {
|
|
const targets = selectedBatchTruncateTargets();
|
|
if (!targets.length) return;
|
|
batchTruncateTargets.value = targets.slice();
|
|
batchTruncateCascade.value = false;
|
|
void refreshBatchTruncatePreviewSql();
|
|
showBatchTruncateConfirm.value = true;
|
|
}
|
|
|
|
function requestBatchEmpty() {
|
|
const targets = selectedBatchEmptyTargets();
|
|
if (!targets.length) return;
|
|
batchEmptyTargets.value = targets.slice();
|
|
batchEmptyPreviewSql.value = "";
|
|
void refreshBatchEmptyPreviewSql(batchEmptyTargets.value)
|
|
.then(() => {
|
|
if (!batchEmptyPreviewSql.value.trim()) throw new Error("Empty table SQL preview is unavailable");
|
|
showBatchEmptyConfirm.value = true;
|
|
})
|
|
.catch((e: any) => {
|
|
batchEmptyTargets.value = [];
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
});
|
|
}
|
|
|
|
function selectedBatchMysqlAutoIncrementTargets(): TreeNode[] {
|
|
const targets = selectedBatchTableTargets();
|
|
return targets.filter((node) => {
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
return supportsNativeMysqlAutoIncrement(config);
|
|
});
|
|
}
|
|
|
|
function batchMysqlAutoIncrementMenuLabel(): string {
|
|
return t("contextMenu.batchMysqlAutoIncrement", { count: selectedBatchMysqlAutoIncrementTargets().length });
|
|
}
|
|
|
|
function batchMysqlAutoIncrementSqlOptionsForNode(node: TreeNode): MysqlAutoIncrementSqlOptions {
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
return {
|
|
databaseType: config?.db_type ?? databaseTypeForNode(node) ?? "mysql",
|
|
driverProfile: config?.driver_profile,
|
|
schema: node.database || node.schema,
|
|
tableName: node.label,
|
|
value: mysqlAutoIncrementValue.value,
|
|
};
|
|
}
|
|
|
|
async function refreshBatchMysqlAutoIncrementPreviewSql() {
|
|
const targets = batchMysqlAutoIncrementTargets.value;
|
|
const statements: string[] = [];
|
|
for (const target of targets) {
|
|
const sql = await buildMysqlAutoIncrementSql(batchMysqlAutoIncrementSqlOptionsForNode(target)).catch(() => "");
|
|
if (sql) statements.push(sql);
|
|
}
|
|
batchMysqlAutoIncrementPreviewSql.value = statements.join("\n");
|
|
}
|
|
|
|
function requestBatchMysqlAutoIncrement() {
|
|
const targets = selectedBatchMysqlAutoIncrementTargets();
|
|
if (!targets.length) return;
|
|
batchMysqlAutoIncrementTargets.value = targets.slice();
|
|
mysqlAutoIncrementValue.value = "1";
|
|
batchMysqlAutoIncrementPreviewSql.value = "";
|
|
void refreshBatchMysqlAutoIncrementPreviewSql();
|
|
showBatchMysqlAutoIncrementConfirm.value = true;
|
|
}
|
|
|
|
async function confirmBatchMysqlAutoIncrement() {
|
|
const targets = batchMysqlAutoIncrementTargets.value.slice();
|
|
if (!targets.length) return;
|
|
const succeeded: TreeNode[] = [];
|
|
let failedCount = 0;
|
|
let firstError: unknown;
|
|
for (const target of targets) {
|
|
if (!target.connectionId || !target.database) {
|
|
failedCount++;
|
|
continue;
|
|
}
|
|
try {
|
|
const config = connectionStore.getConfig(target.connectionId);
|
|
if (!supportsNativeMysqlAutoIncrement(config)) throw new Error("Setting AUTO_INCREMENT is supported only for native MySQL connections.");
|
|
await connectionStore.ensureConnected(target.connectionId);
|
|
const sqlOptions = batchMysqlAutoIncrementSqlOptionsForNode(target);
|
|
const sql = await buildMysqlAutoIncrementSql(sqlOptions);
|
|
await executeTreeNodeSqlWithProductionGuard(target, sql, { database: target.database, schema: target.schema });
|
|
succeeded.push(target);
|
|
} catch (error) {
|
|
failedCount++;
|
|
firstError ??= error;
|
|
}
|
|
}
|
|
if (succeeded.length > 0) {
|
|
await refreshMutatedTableDataTabsForNodes(succeeded);
|
|
}
|
|
if (failedCount > 0 && firstError) {
|
|
toast(t("contextMenu.batchMysqlAutoIncrementPartialFail", { success: succeeded.length, failed: failedCount }), 5000);
|
|
} else if (succeeded.length > 0) {
|
|
toast(t("contextMenu.batchMysqlAutoIncrementSuccess", { count: succeeded.length, value: mysqlAutoIncrementValue.value }), 3000);
|
|
} else if (firstError) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: (firstError as any)?.message || String(firstError) }), 5000);
|
|
}
|
|
batchMysqlAutoIncrementTargets.value = [];
|
|
showBatchMysqlAutoIncrementConfirm.value = false;
|
|
}
|
|
|
|
function requestDropSelectedNodes(): boolean {
|
|
const selected = selectedTreeNodesInVisibleOrder();
|
|
if (selected.length > 1 && selected.some((node) => node.id === activeNode.value.id)) {
|
|
if (!selectedBatchDropTargets().length) return false;
|
|
requestBatchDrop();
|
|
return true;
|
|
}
|
|
return requestDropSelectedNode();
|
|
}
|
|
|
|
function requestDropSelectedNode(): boolean {
|
|
if (activeNode.value.type === "table") {
|
|
dropTable();
|
|
return true;
|
|
}
|
|
if (activeNode.value.type === "view" || activeNode.value.type === "procedure" || activeNode.value.type === "function") {
|
|
requestDropObject();
|
|
return true;
|
|
}
|
|
if (canDropMongoIndex.value) {
|
|
dropMongoIndex();
|
|
return true;
|
|
}
|
|
if (canDropTableChildObject.value) {
|
|
requestDropTableChildObject();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function nodeRenameObjectType(): RenameableObjectType | null {
|
|
if (activeNode.value.type === "table") return "TABLE";
|
|
if (activeNode.value.type === "view") return "VIEW";
|
|
if (activeNode.value.type === "materialized_view") return "MATERIALIZED_VIEW";
|
|
if (activeNode.value.type === "procedure") return "PROCEDURE";
|
|
if (activeNode.value.type === "function") return "FUNCTION";
|
|
return null;
|
|
}
|
|
|
|
const canRenameObject = computed(() => {
|
|
const objectType = nodeRenameObjectType();
|
|
return !!objectType && (supportsObjectRename(currentDatabaseType(), objectType) || supportsSourceBackedRoutineRename(currentDatabaseType(), objectType as any));
|
|
});
|
|
|
|
function openRenameObjectDialog() {
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
renameObjectName.value = activeNode.value.label;
|
|
renameObjectError.value = "";
|
|
renameObjectPreviewSql.value = "";
|
|
showRenameObjectDialog.value = true;
|
|
}
|
|
|
|
function openRenameDatabaseDialog() {
|
|
claimTreeItemDialogOwnership();
|
|
routeTreeItemDialogController();
|
|
renameObjectName.value = activeNode.value.label;
|
|
renameObjectError.value = "";
|
|
renameObjectPreviewSql.value = "";
|
|
showRenameObjectDialog.value = true;
|
|
}
|
|
|
|
async function executeTreeNodeSqlWithProductionGuard(
|
|
node: Pick<TreeNode, "connectionId" | "database" | "schema">,
|
|
sql: string,
|
|
options: {
|
|
database?: string;
|
|
schema?: string;
|
|
executeAsScript?: boolean;
|
|
executionId?: string;
|
|
isCancelledBeforeDispatch?: () => boolean;
|
|
beforeExecute?: () => Promise<void>;
|
|
markDispatched?: () => void;
|
|
} = {},
|
|
) {
|
|
if (!node.connectionId) return undefined;
|
|
const database = options.database ?? node.database ?? "";
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
// Always resolve the connection's configured timeout the same way the
|
|
// main editor does — independent of whether this call also wires up
|
|
// Cancel-button UI. Gating this behind an opt-in flag previously left
|
|
// every non-danger-dialog caller falling back to the backend's hardcoded
|
|
// 30s default instead of the user's configured (or unlimited) timeout.
|
|
const timeoutSecs = queryTimeoutSecsForConnection(config, settingsStore.editorSettings.globalQueryTimeoutSecs);
|
|
const executionId = options.executionId;
|
|
return executeWithProductionSqlGuard({
|
|
connection: config,
|
|
database,
|
|
sql,
|
|
source: t("production.sourceSidebar"),
|
|
execute: async () => {
|
|
// The caller may have observed a cancel click while we were still
|
|
// waiting on connection setup or the production-safety confirmation
|
|
// above — if so, never actually dispatch the SQL to the backend.
|
|
if (options.isCancelledBeforeDispatch?.()) throw new Error("Operation cancelled before it was sent to the database.");
|
|
await options.beforeExecute?.();
|
|
if (options.isCancelledBeforeDispatch?.()) throw new Error("Operation cancelled before it was sent to the database.");
|
|
options.markDispatched?.();
|
|
return options.executeAsScript ? api.executeScript(node.connectionId!, database, sql, options.schema ?? node.schema) : api.executeQuery(node.connectionId!, database, sql, options.schema ?? node.schema, executionId, { timeoutSecs });
|
|
},
|
|
});
|
|
}
|
|
|
|
let renameObjectPreviewRequestId = 0;
|
|
|
|
async function refreshRenameObjectPreviewSql() {
|
|
const node = activeNode.value;
|
|
const requestId = ++renameObjectPreviewRequestId;
|
|
const newName = renameObjectName.value.trim();
|
|
if (!showRenameObjectDialog.value || !newName || newName === node.label) {
|
|
renameObjectPreviewSql.value = "";
|
|
return;
|
|
}
|
|
// Database rename path
|
|
if (node.type === "database") {
|
|
try {
|
|
const sql = await buildRenameDatabaseSql({
|
|
databaseType: currentDatabaseType(),
|
|
oldName: node.label,
|
|
newName,
|
|
});
|
|
if (requestId === renameObjectPreviewRequestId) renameObjectPreviewSql.value = sql;
|
|
} catch {
|
|
if (requestId === renameObjectPreviewRequestId) renameObjectPreviewSql.value = "";
|
|
}
|
|
return;
|
|
}
|
|
const objectType = nodeRenameObjectType();
|
|
if (!objectType) {
|
|
renameObjectPreviewSql.value = "";
|
|
return;
|
|
}
|
|
if (supportsSourceBackedRoutineRename(currentDatabaseType(), objectType as any)) {
|
|
renameObjectPreviewSql.value = `-- Recreate ${objectType} from source, then drop the original object.`;
|
|
return;
|
|
}
|
|
try {
|
|
const sql = await buildRenameObjectSql({
|
|
databaseType: currentDatabaseType(),
|
|
objectType,
|
|
schema: node.schema,
|
|
oldName: node.label,
|
|
newName,
|
|
});
|
|
if (requestId === renameObjectPreviewRequestId) renameObjectPreviewSql.value = sql;
|
|
} catch {
|
|
if (requestId === renameObjectPreviewRequestId) renameObjectPreviewSql.value = "";
|
|
}
|
|
}
|
|
|
|
watch([showRenameObjectDialog, renameObjectName, () => activeNode.value.label, () => activeNode.value.schema, () => activeNode.value.type, () => currentDatabaseType()], () => {
|
|
void refreshRenameObjectPreviewSql();
|
|
});
|
|
|
|
async function confirmRenameObject() {
|
|
const node = sidebarFormTarget.value ?? activeNode.value;
|
|
const newName = renameObjectName.value.trim();
|
|
if (!newName || newName === node.label || !node.connectionId || !node.database) return;
|
|
renameObjectError.value = "";
|
|
let renameApplied = false;
|
|
try {
|
|
const dbType = databaseTypeForNode(node);
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
|
|
// Database rename path
|
|
if (node.type === "database") {
|
|
const maintenanceDatabase = databaseRenameMaintenanceDatabase(connectionStore.getConfig(node.connectionId)?.database, node.label);
|
|
// Preflight: check permissions, prepared transactions, and active connections
|
|
const preflightSql = await buildRenameDatabasePreflightSql({
|
|
databaseType: dbType,
|
|
databaseName: node.label,
|
|
});
|
|
const preflightResult = await api.executeQuery(node.connectionId, maintenanceDatabase, preflightSql);
|
|
let activeConnections = 0;
|
|
let preparedTransactions = 0;
|
|
let isOwner = false;
|
|
if (preflightResult && preflightResult.rows.length > 0) {
|
|
const row = preflightResult.rows[0] as any[];
|
|
activeConnections = Number(row[0]) || 0;
|
|
preparedTransactions = Number(row[1]) || 0;
|
|
isOwner = row[2] === true || row[2] === "t" || String(row[2]) === "true";
|
|
}
|
|
|
|
if (!isOwner) {
|
|
renameObjectError.value = t("contextMenu.renameDatabaseNotOwner", { name: node.label });
|
|
return;
|
|
}
|
|
if (preparedTransactions > 0) {
|
|
renameObjectError.value = t("contextMenu.renameDatabasePreparedTransactions", { name: node.label, count: preparedTransactions });
|
|
return;
|
|
}
|
|
|
|
const sql = await buildRenameDatabaseSql({
|
|
databaseType: dbType,
|
|
oldName: node.label,
|
|
newName,
|
|
terminateConnections: activeConnections > 0,
|
|
});
|
|
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, {
|
|
database: maintenanceDatabase,
|
|
executeAsScript: true,
|
|
beforeExecute: () => connectionStore.closeDatabaseConnection(node.connectionId!, node.label),
|
|
});
|
|
if (executed === undefined) return;
|
|
renameApplied = true;
|
|
toast(t("contextMenu.renameObjectSuccess", { oldName: node.label, newName }), 3000);
|
|
showRenameObjectDialog.value = false;
|
|
const liveNode = findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? node;
|
|
liveNode.label = newName;
|
|
liveNode.database = newName;
|
|
const renamedNode: TreeNode = { ...liveNode, label: newName, database: newName };
|
|
connectionStore.replacePinnedTreeNode(liveNode, renamedNode);
|
|
void connectionStore.refreshTreeNode(liveNode);
|
|
return;
|
|
}
|
|
|
|
const objectType = node.type === "table" ? "TABLE" : node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : node.type === "procedure" ? "PROCEDURE" : node.type === "function" ? "FUNCTION" : null;
|
|
if (!objectType) return;
|
|
if (supportsSourceBackedRoutineRename(dbType, objectType as any)) {
|
|
const schema = node.schema || node.database;
|
|
const source = await api.getObjectSource(node.connectionId, node.database, schema, node.objectName || node.label, objectType as any, node.signature);
|
|
const statements = await buildRoutineRenameObjectSourceStatements({
|
|
databaseType: dbType!,
|
|
objectType: objectType as any,
|
|
schema,
|
|
name: node.label,
|
|
newName,
|
|
source: source.source,
|
|
});
|
|
for (const sql of statements) {
|
|
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema });
|
|
}
|
|
} else {
|
|
const sql = await buildRenameObjectSql({
|
|
databaseType: dbType,
|
|
objectType,
|
|
schema: node.schema,
|
|
oldName: node.label,
|
|
newName,
|
|
});
|
|
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
|
}
|
|
renameApplied = true;
|
|
toast(t("contextMenu.renameObjectSuccess", { oldName: node.label, newName }), 3000);
|
|
showRenameObjectDialog.value = false;
|
|
const renamedNode: TreeNode = { ...node, label: newName, objectName: newName, tableName: newName };
|
|
await refreshTableList(node);
|
|
connectionStore.replacePinnedTreeNode(node, renamedNode);
|
|
} catch (e: any) {
|
|
if (renameApplied) {
|
|
// The database mutation succeeded even when metadata refresh did not;
|
|
// remove the old pin instead of allowing it to revive later.
|
|
connectionStore.removePinnedTreeNodes([node]);
|
|
}
|
|
renameObjectError.value = e?.message || String(e);
|
|
}
|
|
}
|
|
|
|
async function confirmDropObject() {
|
|
const node = sidebarDangerTarget.value ?? activeNode.value;
|
|
if (!node.connectionId || !node.database) return;
|
|
const options = dropObjectSqlOptionsForNode(node);
|
|
if (!options) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const sql = dropObjectPreviewSql.value || (await buildDropObjectSql(options));
|
|
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
|
const msgKey =
|
|
node.type === "view" ? "contextMenu.dropViewSuccess" : node.type === "materialized_view" ? "contextMenu.dropViewSuccess" : node.type === "procedure" ? "contextMenu.dropProcedureSuccess" : node.type === "function" ? "contextMenu.dropFunctionSuccess" : "contextMenu.dropEventSuccess";
|
|
toast(t(msgKey, { name: node.label }), 3000);
|
|
closeDroppedTableObjectTabsForNode(node);
|
|
// Refresh the parent object list so group badges and children stay in sync.
|
|
// Clear the pin first — refresh rebuilds nodes and the old identity must not survive.
|
|
connectionStore.removePinnedTreeNodes([node]);
|
|
try {
|
|
await refreshTableList(node);
|
|
} catch (error: any) {
|
|
// DROP already succeeded; keep the sidebar consistent if metadata refresh fails.
|
|
connectionStore.removeTreeNode(node.id);
|
|
toast(t("contextMenu.objectDropRefreshFailed", { message: error?.message || String(error) }), 5000);
|
|
}
|
|
releaseActiveNodeReference([node.id]);
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function confirmDropTableChildObject() {
|
|
const node = sidebarDangerTarget.value ?? activeNode.value;
|
|
if (!node.connectionId || !node.database) return;
|
|
const options = dropTableChildObjectSqlOptionsForNode(node);
|
|
if (!options) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const sql = dropTableChildObjectPreviewSql.value || (await buildDropTableChildObjectSql(options));
|
|
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
|
toast(t("contextMenu.dropTableChildObjectSuccess", { name: options.name }), 3000);
|
|
connectionStore.removeTreeNode(node.id);
|
|
releaseActiveNodeReference([node.id]);
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function confirmBatchDrop() {
|
|
const targets = batchDropTargets.value.slice();
|
|
if (!targets.length) return;
|
|
try {
|
|
const mongoIndexTargets = targets.filter(canDropMongoIndexNode);
|
|
if (mongoIndexTargets.length) {
|
|
const grouped = new Map<string, TreeNode[]>();
|
|
for (const target of mongoIndexTargets) {
|
|
const key = JSON.stringify([target.connectionId, target.database, target.tableName || ""]);
|
|
const list = grouped.get(key) ?? [];
|
|
list.push(target);
|
|
grouped.set(key, list);
|
|
}
|
|
const mongoIndexGroups = [...grouped.values()];
|
|
// Confirm every production target before changing any collection so a
|
|
// cancellation cannot leave a cross-database batch partially applied.
|
|
for (const groupTargets of mongoIndexGroups) {
|
|
const groupFirst = groupTargets[0];
|
|
if (!groupFirst?.connectionId || !groupFirst.database || !groupFirst.tableName) return;
|
|
const confirmed = await executeWithProductionContextGuard({
|
|
connection: connectionStore.getConfig(groupFirst.connectionId),
|
|
database: groupFirst.database,
|
|
reviewText: groupTargets.map((target) => mongoIndexDropPreview(target, mongoIndexNameForNode(target))).join("\n"),
|
|
source: t("production.sourceSidebar"),
|
|
execute: async () => ({ confirmed: true }),
|
|
});
|
|
if (confirmed === undefined) return;
|
|
}
|
|
|
|
let droppedCount = 0;
|
|
let failedCount = 0;
|
|
let firstGroupError: unknown;
|
|
for (const groupTargets of mongoIndexGroups) {
|
|
const groupFirst = groupTargets[0];
|
|
if (!groupFirst?.connectionId || !groupFirst.database || !groupFirst.tableName) continue;
|
|
const names = groupTargets.map((target) => mongoIndexNameForNode(target));
|
|
try {
|
|
await connectionStore.ensureConnected(groupFirst.connectionId);
|
|
const result = await api.mongoDropIndexes(groupFirst.connectionId, groupFirst.database, groupFirst.tableName, JSON.stringify(names.length === 1 ? names[0] : names), names.length === 1);
|
|
const dropped = new Set(result.dropped_names);
|
|
droppedCount += result.dropped_names.length;
|
|
failedCount += mongoDropIndexFailureCount(result);
|
|
for (const target of groupTargets) {
|
|
const indexName = mongoIndexNameForNode(target);
|
|
if (!dropped.has(indexName)) continue;
|
|
connectionStore.removeTreeNode(target.id);
|
|
releaseActiveNodeReference([target.id]);
|
|
}
|
|
} catch (error) {
|
|
// A transport-level failure has no per-index payload; retain prior
|
|
// successes and continue with independent collection groups.
|
|
failedCount += groupTargets.length;
|
|
firstGroupError ??= error;
|
|
} finally {
|
|
await refreshMongoIndexTreeAfterMutation(groupFirst);
|
|
}
|
|
}
|
|
if (droppedCount === 0 && firstGroupError && failedCount === mongoIndexTargets.length) throw firstGroupError;
|
|
if (failedCount > 0) {
|
|
toast(t("contextMenu.dropIndexesPartialFailure", { success: droppedCount, failed: failedCount }), 5000);
|
|
} else {
|
|
toast(t("contextMenu.batchDropSuccess", { count: droppedCount }), 3000);
|
|
}
|
|
showBatchDropConfirm.value = false;
|
|
return;
|
|
}
|
|
const useCascade = batchDropCascade.value && targets.every((node) => node.type !== "table" || supportsDropTableCascade(databaseTypeForNode(node)));
|
|
if (targets.every((node) => node.type === "table" && node.connectionId && node.database)) {
|
|
const first = targets[0]!;
|
|
await connectionStore.ensureConnected(first.connectionId!);
|
|
batchDropProgress.value = { completed: 0, total: targets.length };
|
|
const plan = await Promise.all(
|
|
targets.map(async (target) => {
|
|
const sql = await dropSqlForTreeNode(target, { cascade: useCascade });
|
|
if (!sql) throw new Error("Drop table SQL is unavailable");
|
|
return { target, sql };
|
|
}),
|
|
);
|
|
const batchSql = plan.map(({ sql }) => sql).join(";\n");
|
|
const result = await executeWithProductionSqlGuard({
|
|
connection: connectionStore.getConfig(first.connectionId!),
|
|
database: first.database!,
|
|
sql: batchSql,
|
|
source: t("production.sourceSidebar"),
|
|
execute: () =>
|
|
runBatchTableDrop({
|
|
databaseType: databaseTypeForNode(first),
|
|
plan,
|
|
executeStatement: (sql) => api.executeQuery(first.connectionId!, first.database!, sql),
|
|
executeBatch: (sql, onProgress) => api.executeMultiWithProgress(first.connectionId!, first.database!, sql, onProgress),
|
|
onProgress: (progress) => {
|
|
batchDropProgress.value = { completed: Math.min(progress.completed, targets.length), total: targets.length };
|
|
},
|
|
}),
|
|
});
|
|
if (!result) return;
|
|
|
|
for (const target of result.succeeded) {
|
|
closeDroppedTableObjectTabsForNode(target);
|
|
connectionStore.removeTreeNode(target.id);
|
|
releaseActiveNodeReference([target.id]);
|
|
}
|
|
if (result.failed) throw result.failed;
|
|
toast(t("contextMenu.batchDropSuccess", { count: result.succeeded.length }), 3000);
|
|
showBatchDropConfirm.value = false;
|
|
return;
|
|
}
|
|
const refreshScopes = new Map<string, TreeNode>();
|
|
for (const target of targets) {
|
|
if (!target.connectionId || !target.database) continue;
|
|
await connectionStore.ensureConnected(target.connectionId);
|
|
const sql = await dropSqlForTreeNode(target, { cascade: useCascade });
|
|
if (!sql) continue;
|
|
await executeTreeNodeSqlWithProductionGuard(target, sql, { database: target.database, schema: target.schema });
|
|
closeDroppedTableObjectTabsForNode(target);
|
|
// Remove immediately so a later failure cannot leave dropped objects in the tree.
|
|
connectionStore.removeTreeNode(target.id);
|
|
releaseActiveNodeReference([target.id]);
|
|
refreshScopes.set(`${target.connectionId}:${target.database}:${target.schema ?? ""}`, target);
|
|
}
|
|
toast(t("contextMenu.batchDropSuccess", { count: targets.length }), 3000);
|
|
showBatchDropConfirm.value = false;
|
|
const refreshResults = await Promise.allSettled([...refreshScopes.values()].map((target) => refreshTableList(target)));
|
|
const refreshFailure = refreshResults.find((result): result is PromiseRejectedResult => result.status === "rejected");
|
|
if (refreshFailure) {
|
|
toast(t("contextMenu.objectDropRefreshFailed", { message: refreshFailure.reason?.message || String(refreshFailure.reason) }), 5000);
|
|
}
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function confirmBatchTruncate() {
|
|
const targets = batchTruncateTargets.value.slice();
|
|
if (!targets.length) return;
|
|
try {
|
|
const useCascade = batchTruncateCascade.value && targets.every((node) => supportsTruncateTableCascade(databaseTypeForNode(node)));
|
|
await runBatchTableTruncate(
|
|
targets,
|
|
async (target) => {
|
|
if (!target.connectionId || !target.database) return false;
|
|
await connectionStore.ensureConnected(target.connectionId);
|
|
const sql = await truncateSqlForTreeNode(target, { cascade: useCascade });
|
|
if (!sql) return false;
|
|
const result = await executeTreeNodeSqlWithProductionGuard(target, sql, { database: target.database, schema: target.schema });
|
|
return result === undefined ? false : undefined;
|
|
},
|
|
refreshMutatedTableDataTabsForNodes,
|
|
);
|
|
toast(t("contextMenu.batchTruncateSuccess", { count: targets.length }), 3000);
|
|
showBatchTruncateConfirm.value = false;
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function confirmBatchEmpty() {
|
|
const targets = batchEmptyTargets.value.slice();
|
|
if (!targets.length) return;
|
|
const asynchronousMutation = targets.every((target) => databaseTypeForNode(target) === "clickhouse");
|
|
const result = await runBatchTableEmpty(targets, async (target) => {
|
|
if (!target.connectionId || !target.database) throw new Error("Missing table connection context");
|
|
await connectionStore.ensureConnected(target.connectionId);
|
|
const sql = await emptySqlForTreeNode(target);
|
|
if (!sql) throw new Error("Empty table SQL is unavailable");
|
|
await executeTreeNodeSqlWithProductionGuard(target, sql, { database: target.database, schema: target.schema });
|
|
});
|
|
for (const failure of result.failed) {
|
|
console.error(`Failed to empty table "${failure.target.label}":`, failure.error);
|
|
}
|
|
const feedback = batchTableEmptyFeedback(result, asynchronousMutation);
|
|
if (feedback === "success") {
|
|
toast(t("contextMenu.batchEmptySuccess", { count: result.succeeded.length }), 3000);
|
|
} else if (feedback === "submitted") {
|
|
toast(t("contextMenu.batchEmptySubmitted", { count: result.succeeded.length }), 3000);
|
|
} else if (feedback === "submitted-partial") {
|
|
toast(t("contextMenu.batchEmptySubmittedPartial", { success: result.succeeded.length, failed: result.failed.length }), 5000);
|
|
} else {
|
|
toast(t("contextMenu.batchEmptyPartialFail", { success: result.succeeded.length, failed: result.failed.length }), 5000);
|
|
}
|
|
await refreshMutatedTableDataTabsForNodes(result.succeeded);
|
|
batchEmptyTargets.value = [];
|
|
showBatchEmptyConfirm.value = false;
|
|
}
|
|
|
|
const canCreateTable = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
const supportsHBaseTableCreation = config?.db_type === "hbase" && !connectionIsEffectivelyReadOnly(config);
|
|
return (
|
|
(activeNode.value.type === "database" || activeNode.value.type === "schema" || activeNode.value.type === "group-tables") &&
|
|
!isSqlServerLinkedNode(activeNode.value) &&
|
|
!!activeNode.value.database &&
|
|
(supportsHBaseTableCreation || supportsTableStructureEditing(tableStructureDatabaseTypeForConnection(config)))
|
|
);
|
|
});
|
|
|
|
const canCreateDatabase = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return activeNode.value.type === "connection" && canCreateConnectionNamespace(config);
|
|
});
|
|
|
|
const canCreateNacosNamespace = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return activeNode.value.type === "connection" && config?.db_type === "nacos" && !connectionIsEffectivelyReadOnly(config);
|
|
});
|
|
|
|
const canEditNacosNamespace = computed(() => {
|
|
if (activeNode.value.type !== "nacos-namespace" || !activeNode.value.connectionId || !activeNode.value.nacosNamespace) return false;
|
|
const config = connectionStore.getConfig(activeNode.value.connectionId);
|
|
return config?.db_type === "nacos" && !connectionIsEffectivelyReadOnly(config);
|
|
});
|
|
|
|
const canDeleteNacosNamespace = computed(() => canEditNacosNamespace.value);
|
|
|
|
const isDuckDbConnection = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return activeNode.value.type === "connection" && config?.db_type === "duckdb" && connectionNamespaceCreationTarget(config) === "attach";
|
|
});
|
|
|
|
const isSqliteAttachConnection = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return activeNode.value.type === "connection" && config?.db_type === "sqlite" && connectionNamespaceCreationTarget(config) === "attach";
|
|
});
|
|
|
|
const isConnectionSchemaCreation = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return activeNode.value.type === "connection" && connectionNamespaceCreationTarget(config) === "schema";
|
|
});
|
|
|
|
const canSetCreateDatabaseCharset = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return connectionNamespaceCreationTarget(config) === "database" && supportsCreateDatabaseCharset(config?.db_type, config?.driver_profile);
|
|
});
|
|
|
|
const canSetCreateDatabaseLocale = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return connectionNamespaceCreationTarget(config) === "database" && supportsCreateDatabaseLocale(config?.db_type, config?.driver_profile);
|
|
});
|
|
|
|
const canDropDatabase = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return activeNode.value.type === "database" && !isSqlServerLinkedNode(activeNode.value) && (supportsDatabaseCreation(config?.db_type) || supportsCreateDatabaseLocale(config?.db_type, config?.driver_profile));
|
|
});
|
|
|
|
const databasePropertyGroups = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return editableDatabasePropertyGroups(config, activeNode.value);
|
|
});
|
|
|
|
const canEditDatabaseProperties = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return canEditDatabasePropertiesForNode(config, activeNode.value) && !isSqlServerLinkedNode(activeNode.value);
|
|
});
|
|
|
|
const canEditDatabaseCharsetCollation = computed(() => databasePropertyGroups.value.includes("charsetCollation"));
|
|
|
|
const canEditDatabaseComment = computed(() => databasePropertyGroups.value.includes("databaseComment"));
|
|
|
|
const canRenameDatabase = computed(() => {
|
|
const node = activeNode.value;
|
|
if (node.type !== "database" || !node.database) return false;
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
return !!config && !connectionIsEffectivelyReadOnly(config) && supportsDatabaseRename(config.db_type);
|
|
});
|
|
|
|
const renameObjectDialogTitle = computed(() => {
|
|
if (activeNode.value.type === "database") return t("contextMenu.renameDatabaseTitle");
|
|
return t("contextMenu.renameObjectTitle");
|
|
});
|
|
|
|
const canCreateSchema = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return canCreateDatabaseNodeNamespace(config, activeNode.value) && !isSqlServerLinkedNode(activeNode.value) && !connectionUsesDatabaseObjectTreeMode(config);
|
|
});
|
|
|
|
const canDropSchema = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
const dbType = effectiveDatabaseTypeForConnection(config);
|
|
return activeNode.value.type === "schema" && !isXuguSyntheticTreeNode(config?.db_type, activeNode.value.type, activeNode.value.schema) && !isSqlServerLinkedNode(activeNode.value) && (usesTreeSchemaMode(dbType) || dbType === "dameng") && !connectionUsesDatabaseObjectTreeMode(config);
|
|
});
|
|
|
|
const canEditSchemaComment = computed(() => {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
return activeNode.value.type === "schema" && !!activeNode.value.database && !isXuguSyntheticTreeNode(config?.db_type, activeNode.value.type, activeNode.value.schema) && !connectionIsEffectivelyReadOnly(config) && supportsSchemaComment(effectiveDatabaseTypeForConnection(config));
|
|
});
|
|
|
|
const canBatchDropCascade = computed(() => {
|
|
const targets = selectedBatchTableTargets();
|
|
return targets.length > 1 && targets.every((node) => supportsDropTableCascade(databaseTypeForNode(node)));
|
|
});
|
|
|
|
const canBatchTruncateCascade = computed(() => {
|
|
const targets = selectedBatchTruncateTargets();
|
|
return targets.length > 1 && targets.every((node) => supportsTruncateTableCascade(databaseTypeForNode(node)));
|
|
});
|
|
|
|
async function refreshDropDatabasePreviewSql() {
|
|
const node = activeNode.value;
|
|
if (node.type === "mongo-db") {
|
|
dropDatabasePreviewSql.value = `db.getSiblingDB(${JSON.stringify(node.label)}).dropDatabase();`;
|
|
return;
|
|
}
|
|
if (node.type === "vector-database") {
|
|
dropDatabasePreviewSql.value = `POST /v2/vectordb/databases/drop\n{"dbName":${JSON.stringify(node.database || node.label)}}`;
|
|
return;
|
|
}
|
|
dropDatabasePreviewSql.value = "";
|
|
dropDatabasePreviewSql.value = await buildDropDatabaseSql({
|
|
databaseType: currentDatabaseType(),
|
|
name: node.label,
|
|
}).catch(() => "");
|
|
}
|
|
|
|
async function refreshDropSchemaPreviewSql() {
|
|
const node = activeNode.value;
|
|
dropSchemaPreviewSql.value = "";
|
|
dropSchemaPreviewSql.value = await buildDropSchemaSql({
|
|
databaseType: currentDatabaseType(),
|
|
name: node.label,
|
|
}).catch(() => "");
|
|
}
|
|
|
|
function databasePropertyName(): string {
|
|
return activeNode.value.database || activeNode.value.label;
|
|
}
|
|
|
|
function resultColumnValue(result: { columns?: string[]; rows?: unknown[] }, names: string[]): string {
|
|
const firstRow = result.rows?.[0];
|
|
if (Array.isArray(firstRow)) {
|
|
const lowerNames = names.map((name) => name.toLowerCase());
|
|
const index = Math.max(0, result.columns?.findIndex((column) => lowerNames.includes(column.toLowerCase())) ?? 0);
|
|
return firstRow[index] == null ? "" : String(firstRow[index]);
|
|
}
|
|
if (firstRow && typeof firstRow === "object") {
|
|
const record = firstRow as Record<string, unknown>;
|
|
const key = Object.keys(record).find((column) => names.some((name) => name.toLowerCase() === column.toLowerCase()));
|
|
const value = key ? record[key] : undefined;
|
|
return value == null ? "" : String(value);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function databasePropertyEditOptions() {
|
|
if (!canEditDatabaseProperties.value) return null;
|
|
const base = {
|
|
databaseType: currentDatabaseType(),
|
|
driverProfile: activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId)?.driver_profile : undefined,
|
|
target: "database" as const,
|
|
name: databasePropertyName(),
|
|
};
|
|
if (canEditDatabaseCharsetCollation.value) {
|
|
return {
|
|
...base,
|
|
charset: editDatabaseCharset.value,
|
|
collation: editDatabaseCollation.value,
|
|
};
|
|
}
|
|
if (canEditDatabaseComment.value) {
|
|
return {
|
|
...base,
|
|
comment: editDatabaseCommentText.value,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function refreshEditDatabasePropertiesPreviewSql() {
|
|
editDatabasePropertiesPreviewSql.value = "";
|
|
const options = databasePropertyEditOptions();
|
|
if (!options) return;
|
|
editDatabasePropertiesPreviewSql.value = await buildUpdateDatabasePropertiesSql(options).catch(() => "");
|
|
}
|
|
|
|
async function loadDatabaseCharsetProperties() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
const sql = `SELECT DEFAULT_CHARACTER_SET_NAME AS charset, DEFAULT_COLLATION_NAME AS collation FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = '${databasePropertyName().replace(/'/g, "''")}';`;
|
|
const result = await api.executeQuery(node.connectionId, databasePropertyName(), sql, undefined, undefined, { maxRows: 1 });
|
|
const charset = resultColumnValue(result, ["charset", "DEFAULT_CHARACTER_SET_NAME"]);
|
|
const collation = resultColumnValue(result, ["collation", "DEFAULT_COLLATION_NAME"]);
|
|
if (charset) editDatabaseCharset.value = charset;
|
|
if (collation) editDatabaseCollation.value = collation;
|
|
}
|
|
|
|
async function loadDatabaseCommentProperty() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
const sql = buildGetDatabaseCommentSql({
|
|
databaseType: currentDatabaseType(),
|
|
name: databasePropertyName(),
|
|
});
|
|
const result = await api.executeQuery(node.connectionId, databasePropertyName(), sql, undefined, undefined, { maxRows: 1 });
|
|
editDatabaseCommentText.value = resultColumnValue(result, ["comment"]);
|
|
}
|
|
|
|
async function openEditDatabasePropertiesDialog() {
|
|
const node = activeNode.value;
|
|
if (!canEditDatabaseProperties.value || !node.connectionId) return;
|
|
editDatabasePropertiesLoading.value = true;
|
|
editDatabaseCharset.value = "utf8mb4";
|
|
editDatabaseCollation.value = "utf8mb4_unicode_ci";
|
|
editDatabaseCommentText.value = "";
|
|
editDatabasePropertiesPreviewSql.value = "";
|
|
createDatabaseCharsetOptions.value = fallbackCreateDatabaseCharset.charsets;
|
|
createDatabaseCollationsByCharset.value = fallbackCreateDatabaseCharset.collationsByCharset;
|
|
showEditDatabasePropertiesDialog.value = true;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
if (canEditDatabaseCharsetCollation.value) {
|
|
await loadCreateDatabaseCharsetMetadata("edit");
|
|
await loadDatabaseCharsetProperties().catch(() => undefined);
|
|
} else if (canEditDatabaseComment.value) {
|
|
await loadDatabaseCommentProperty().catch(() => undefined);
|
|
}
|
|
await refreshEditDatabasePropertiesPreviewSql();
|
|
} finally {
|
|
editDatabasePropertiesLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function confirmEditDatabaseProperties() {
|
|
const node = sidebarFormTarget.value ?? activeNode.value;
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
const propertyGroups = editableDatabasePropertyGroups(config, node);
|
|
if (!propertyGroups.length || !node.connectionId || editDatabasePropertiesLoading.value) return;
|
|
editDatabasePropertiesLoading.value = true;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const base = {
|
|
databaseType: databaseTypeForNode(node),
|
|
driverProfile: config?.driver_profile,
|
|
target: "database" as const,
|
|
name: node.database || node.label,
|
|
};
|
|
const options = propertyGroups.includes("charsetCollation") ? { ...base, charset: editDatabaseCharset.value, collation: editDatabaseCollation.value } : propertyGroups.includes("databaseComment") ? { ...base, comment: editDatabaseCommentText.value } : null;
|
|
if (!options) return;
|
|
const sql = await buildUpdateDatabasePropertiesSql(options);
|
|
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database || node.label });
|
|
toast(t("contextMenu.editDatabasePropertiesSuccess", { name: node.label }), 3000);
|
|
showEditDatabasePropertiesDialog.value = false;
|
|
await connectionStore.loadDatabases(node.connectionId, { force: true });
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, e) }), 5000);
|
|
} finally {
|
|
editDatabasePropertiesLoading.value = false;
|
|
}
|
|
}
|
|
|
|
watch([editDatabaseCharset, editDatabaseCollation, editDatabaseCommentText], () => {
|
|
if (showEditDatabasePropertiesDialog.value) void refreshEditDatabasePropertiesPreviewSql();
|
|
});
|
|
|
|
async function refreshSchemaCommentPreviewSql() {
|
|
const node = activeNode.value;
|
|
if (!canEditSchemaComment.value) {
|
|
schemaCommentPreviewSql.value = "";
|
|
return;
|
|
}
|
|
schemaCommentPreviewSql.value = await buildUpdateDatabasePropertiesSql({
|
|
databaseType: currentDatabaseType(),
|
|
target: "schema",
|
|
name: node.schema || node.label,
|
|
comment: schemaCommentText.value,
|
|
}).catch(() => "");
|
|
}
|
|
|
|
watch(schemaCommentText, () => {
|
|
if (showEditSchemaCommentDialog.value) void refreshSchemaCommentPreviewSql();
|
|
});
|
|
|
|
function schemaCommentFromResult(result: { columns?: string[]; rows?: unknown[] }): string {
|
|
return resultColumnValue(result, ["comment"]);
|
|
}
|
|
|
|
async function openEditSchemaCommentDialog() {
|
|
const node = activeNode.value;
|
|
if (!canEditSchemaComment.value || !node.connectionId || !node.database) return;
|
|
schemaCommentText.value = "";
|
|
schemaCommentLoading.value = true;
|
|
showEditSchemaCommentDialog.value = true;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const sql = buildGetSchemaCommentSql({
|
|
databaseType: currentDatabaseType(),
|
|
name: node.schema || node.label,
|
|
});
|
|
const result = await api.executeQuery(node.connectionId, node.database, sql, node.schema, undefined, { maxRows: 1 });
|
|
schemaCommentText.value = schemaCommentFromResult(result);
|
|
await refreshSchemaCommentPreviewSql();
|
|
} catch {
|
|
schemaCommentText.value = "";
|
|
await refreshSchemaCommentPreviewSql();
|
|
} finally {
|
|
schemaCommentLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function confirmEditSchemaComment() {
|
|
const node = sidebarFormTarget.value ?? activeNode.value;
|
|
if (!canEditSchemaComment.value || !node.connectionId || !node.database || schemaCommentLoading.value) return;
|
|
schemaCommentLoading.value = true;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const sql = await buildUpdateDatabasePropertiesSql({
|
|
databaseType: databaseTypeForNode(node),
|
|
target: "schema",
|
|
name: node.schema || node.label,
|
|
comment: schemaCommentText.value,
|
|
});
|
|
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
|
toast(t("contextMenu.editSchemaCommentSuccess", { name: node.label }), 3000);
|
|
showEditSchemaCommentDialog.value = false;
|
|
await connectionStore.loadSchemas(node.connectionId, node.database, { force: true });
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: translateBackendError(t, e) }), 5000);
|
|
} finally {
|
|
schemaCommentLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function openCreateDatabase() {
|
|
if (isDuckDbConnection.value) {
|
|
await createDuckDbAttachedDatabaseFile();
|
|
return;
|
|
}
|
|
if (isSqliteAttachConnection.value) {
|
|
await attachSqliteDatabaseFile();
|
|
return;
|
|
}
|
|
openCreateDatabaseDialog();
|
|
}
|
|
|
|
function openCreateDatabaseDialog() {
|
|
createDatabaseName.value = "";
|
|
createDatabaseCharset.value = "utf8mb4";
|
|
createDatabaseCollation.value = "utf8mb4_unicode_ci";
|
|
createDatabaseUsers.value = [];
|
|
createDatabaseSelectedUsers.value = [];
|
|
createDatabaseUsersLoading.value = false;
|
|
createDatabaseAuthorizationPlan.value = undefined;
|
|
createDatabasePreviewSql.value = "";
|
|
createDatabaseAuthorizationResults.value = [];
|
|
createDatabaseCharsetOptions.value = fallbackCreateDatabaseCharset.charsets;
|
|
createDatabaseCollationsByCharset.value = fallbackCreateDatabaseCharset.collationsByCharset;
|
|
showCreateDatabaseDialog.value = true;
|
|
if (canSetCreateDatabaseLocale.value) {
|
|
// GBase 8s / Informix: the "charset" control selects the new database's DB_LOCALE, which the
|
|
// agent applies by opening the CREATE DATABASE session with it. Seed a UTF-8 default, then
|
|
// repopulate the options with the collations already present on the connected instance.
|
|
createDatabaseCharsetOptions.value = [...GBASE8S_DATABASE_LOCALES];
|
|
createDatabaseCollationsByCharset.value = {};
|
|
createDatabaseCharset.value = DEFAULT_GBASE8S_DATABASE_LOCALE;
|
|
createDatabaseCollation.value = "";
|
|
void loadGbase8sDatabaseLocales();
|
|
} else if (canSetCreateDatabaseCharset.value) {
|
|
void loadCreateDatabaseCharsetMetadata();
|
|
}
|
|
void loadCreateDatabaseUsers();
|
|
}
|
|
|
|
let createDatabaseUsersRequestId = 0;
|
|
|
|
async function loadCreateDatabaseUsers() {
|
|
const node = sidebarFormTarget.value ?? activeNode.value;
|
|
if (!node.connectionId) return;
|
|
const connectionId = node.connectionId;
|
|
const requestId = ++createDatabaseUsersRequestId;
|
|
const requestOwner = sidebarTreeDialogOwner.value;
|
|
const requestIsActive = () => requestId === createDatabaseUsersRequestId && sidebarTreeDialogOwner.value === requestOwner && sidebarFormTarget.value?.connectionId === connectionId && showCreateDatabaseDialog.value;
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const userProvider = resolveDatabaseUserAdminProviderForConnection(config);
|
|
if (!userProvider) return;
|
|
createDatabaseUsersLoading.value = true;
|
|
try {
|
|
await connectionStore.ensureConnected(connectionId);
|
|
try {
|
|
const result = await api.executeQuery(connectionId, "", userProvider.listUsersSql(), undefined, undefined, { maxRows: 5000 });
|
|
if (requestIsActive()) createDatabaseUsers.value = userProvider.parseUsers(result);
|
|
} catch (error) {
|
|
if (!userProvider.fallbackListUsersSql || !userProvider.parseFallbackUsers) throw error;
|
|
const result = await api.executeQuery(connectionId, "", userProvider.fallbackListUsersSql(), undefined, undefined, { maxRows: 5000 });
|
|
if (requestIsActive()) createDatabaseUsers.value = userProvider.parseFallbackUsers(result);
|
|
}
|
|
} catch (error: any) {
|
|
if (requestIsActive()) {
|
|
createDatabaseUsers.value = [];
|
|
toast(t("contextMenu.createDatabaseUsersLoadFailed", { message: error?.message || String(error) }), 5000);
|
|
}
|
|
} finally {
|
|
if (requestIsActive()) createDatabaseUsersLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function createDatabaseUserKey(user: DatabaseUserIdentity): string {
|
|
return `${user.user}\u0000${user.host}`;
|
|
}
|
|
|
|
function createDatabaseUserLabel(user: DatabaseUserIdentity): string {
|
|
const node = sidebarFormTarget.value ?? activeNode.value;
|
|
const provider = resolveDatabaseUserAdminProviderForConnection(connectionStore.getConfig(node.connectionId || ""));
|
|
return provider?.label(user) ?? user.user;
|
|
}
|
|
|
|
function createDatabaseUserSelected(user: DatabaseUserIdentity): boolean {
|
|
const key = createDatabaseUserKey(user);
|
|
return createDatabaseSelectedUsers.value.some((selected) => createDatabaseUserKey(selected) === key);
|
|
}
|
|
|
|
function toggleCreateDatabaseUser(user: DatabaseUserIdentity) {
|
|
const key = createDatabaseUserKey(user);
|
|
if (createDatabaseSelectedUsers.value.some((selected) => createDatabaseUserKey(selected) === key)) {
|
|
createDatabaseSelectedUsers.value = createDatabaseSelectedUsers.value.filter((selected) => createDatabaseUserKey(selected) !== key);
|
|
} else {
|
|
createDatabaseSelectedUsers.value = [...createDatabaseSelectedUsers.value, user];
|
|
}
|
|
}
|
|
|
|
function openConnectionNamespaceCreation() {
|
|
if (isConnectionSchemaCreation.value) {
|
|
openCreateSchemaDialog();
|
|
return;
|
|
}
|
|
void openCreateDatabase();
|
|
}
|
|
|
|
function connectionNamespaceCreationLabel() {
|
|
if (isDuckDbConnection.value) return t("contextMenu.createDuckDbFile");
|
|
if (isSqliteAttachConnection.value) return t("contextMenu.attachSqliteDatabase");
|
|
if (isConnectionSchemaCreation.value) return t("contextMenu.createSchema");
|
|
return t("contextMenu.createDatabase");
|
|
}
|
|
|
|
function updateCreateDatabaseCharset(value: string) {
|
|
const previousCharset = createDatabaseCharset.value;
|
|
createDatabaseCharset.value = value;
|
|
createDatabaseCollation.value = nextCreateDatabaseCollation(value, previousCharset, createDatabaseCollation.value, createDatabaseCollationsByCharset.value);
|
|
}
|
|
|
|
function updateEditDatabaseCharset(value: string) {
|
|
const previousCharset = editDatabaseCharset.value;
|
|
editDatabaseCharset.value = value;
|
|
editDatabaseCollation.value = nextCreateDatabaseCollation(value, previousCharset, editDatabaseCollation.value, createDatabaseCollationsByCharset.value);
|
|
}
|
|
|
|
async function loadCreateDatabaseCharsetMetadata(target: "create" | "edit" = "create") {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId || createDatabaseCharsetLoading.value) return;
|
|
createDatabaseCharsetLoading.value = true;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const [charsetResult, collationResult] = await Promise.all([api.executeQuery(node.connectionId, "", "SHOW CHARACTER SET", undefined, undefined, { maxRows: 1000 }), api.executeQuery(node.connectionId, "", "SHOW COLLATION", undefined, undefined, { maxRows: 10000 })]);
|
|
if (target === "create" && !showCreateDatabaseDialog.value) return;
|
|
if (target === "edit" && !showEditDatabasePropertiesDialog.value) return;
|
|
const metadata = parseCreateDatabaseCharsetMetadata(charsetResult, collationResult);
|
|
createDatabaseCharsetOptions.value = metadata.charsets;
|
|
createDatabaseCollationsByCharset.value = metadata.collationsByCharset;
|
|
const selectedCharset = target === "create" ? createDatabaseCharset.value : editDatabaseCharset.value;
|
|
if (!createDatabaseCharsetOptions.value.includes(selectedCharset) && createDatabaseCharsetOptions.value.length) {
|
|
if (target === "create") {
|
|
updateCreateDatabaseCharset(createDatabaseCharsetOptions.value[0]);
|
|
} else {
|
|
updateEditDatabaseCharset(createDatabaseCharsetOptions.value[0]);
|
|
}
|
|
} else {
|
|
if (target === "create") {
|
|
createDatabaseCollation.value = nextCreateDatabaseCollation(createDatabaseCharset.value, createDatabaseCharset.value, createDatabaseCollation.value, createDatabaseCollationsByCharset.value);
|
|
} else {
|
|
editDatabaseCollation.value = nextCreateDatabaseCollation(editDatabaseCharset.value, editDatabaseCharset.value, editDatabaseCollation.value, createDatabaseCollationsByCharset.value);
|
|
}
|
|
}
|
|
} catch {
|
|
createDatabaseCharsetOptions.value = fallbackCreateDatabaseCharset.charsets;
|
|
createDatabaseCollationsByCharset.value = fallbackCreateDatabaseCharset.collationsByCharset;
|
|
} finally {
|
|
createDatabaseCharsetLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadGbase8sDatabaseLocales() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
createDatabaseCharsetLoading.value = true;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const result = await api.executeQuery(node.connectionId, "", "SELECT DISTINCT dbs_collate FROM sysmaster:sysdbslocale ORDER BY 1", undefined, undefined, { maxRows: 500 });
|
|
if (!showCreateDatabaseDialog.value) return;
|
|
const idx = result.columns.findIndex((column) => column.trim().toLowerCase() === "dbs_collate");
|
|
const locales = [...new Set(result.rows.map((row) => String(row[idx >= 0 ? idx : 0] ?? "").trim()).filter(Boolean))];
|
|
if (!locales.length) throw new Error("no collations");
|
|
createDatabaseCharsetOptions.value = locales;
|
|
createDatabaseCollationsByCharset.value = {};
|
|
if (!locales.includes(createDatabaseCharset.value)) {
|
|
updateCreateDatabaseCharset(defaultGbase8sDatabaseLocale(locales));
|
|
}
|
|
} catch {
|
|
createDatabaseCharsetOptions.value = [...GBASE8S_DATABASE_LOCALES];
|
|
createDatabaseCollationsByCharset.value = {};
|
|
if (!createDatabaseCharsetOptions.value.includes(createDatabaseCharset.value)) {
|
|
updateCreateDatabaseCharset(DEFAULT_GBASE8S_DATABASE_LOCALE);
|
|
}
|
|
} finally {
|
|
createDatabaseCharsetLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function ensureDuckDbFileExtension(path: string): string {
|
|
return /\.(duckdb|db)$/i.test(path) ? path : `${path}.duckdb`;
|
|
}
|
|
|
|
async function createDuckDbAttachedDatabaseFile() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
if (!isTauriRuntime()) {
|
|
toast(t("contextMenu.createDuckDbFileDesktopOnly"), 4000);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { save } = await import("@tauri-apps/plugin-dialog");
|
|
const selectedPath = await save({
|
|
defaultPath: "database.duckdb",
|
|
filters: [{ name: "DuckDB", extensions: ["duckdb", "db"] }],
|
|
});
|
|
if (!selectedPath) return;
|
|
|
|
const path = ensureDuckDbFileExtension(selectedPath);
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const existingDatabases = await api.listDatabases(node.connectionId);
|
|
const name = uniqueAttachedDatabaseName(
|
|
attachedDatabaseNameFromPath(path, "duckdb_database"),
|
|
existingDatabases.map((database) => database.name),
|
|
);
|
|
const sql = await buildDuckDbAttachDatabaseSql(path, name);
|
|
const executionResult = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: "" });
|
|
if (executionResult === undefined) return;
|
|
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
if (config) {
|
|
await connectionStore.updateConnection({
|
|
...config,
|
|
attached_databases: [...(config.attached_databases ?? []), { name, path }],
|
|
});
|
|
}
|
|
await connectionStore.ensureVisibleDatabase(node.connectionId, name);
|
|
await connectionStore.loadDatabases(node.connectionId, { force: true });
|
|
connectionStore.selectedTreeNodeId = `${node.connectionId}:${name}`;
|
|
toast(t("contextMenu.createDuckDbFileSuccess", { name }), 3000);
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function attachSqliteDatabaseFile() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId) return;
|
|
if (!isTauriRuntime()) {
|
|
toast(t("contextMenu.attachSqliteDatabaseDesktopOnly"), 4000);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { open } = await import("@tauri-apps/plugin-dialog");
|
|
const selected = await open({
|
|
title: t("contextMenu.attachSqliteDatabase"),
|
|
multiple: false,
|
|
filters: [{ name: "SQLite", extensions: SQLITE_DATABASE_FILE_EXTENSIONS }],
|
|
});
|
|
const path = Array.isArray(selected) ? selected[0] : selected;
|
|
if (!path || typeof path !== "string") return;
|
|
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const existingDatabases = await api.listDatabases(node.connectionId);
|
|
const name = uniqueAttachedDatabaseName(
|
|
attachedDatabaseNameFromPath(path, "sqlite_database"),
|
|
existingDatabases.map((database) => database.name),
|
|
["main", "temp"],
|
|
);
|
|
const sql = await buildSqliteAttachDatabaseSql(path, name);
|
|
const executionResult = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: "" });
|
|
if (executionResult === undefined) return;
|
|
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
if (config) {
|
|
await connectionStore.updateConnection({
|
|
...config,
|
|
attached_databases: [...(config.attached_databases ?? []), { name, path }],
|
|
});
|
|
}
|
|
await connectionStore.ensureVisibleDatabase(node.connectionId, name);
|
|
await connectionStore.loadDatabases(node.connectionId, { force: true });
|
|
connectionStore.selectedTreeNodeId = `${node.connectionId}:${name}`;
|
|
toast(t("contextMenu.attachSqliteDatabaseSuccess", { name }), 3000);
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function confirmCreateDatabase() {
|
|
const node = sidebarFormTarget.value ?? activeNode.value;
|
|
const name = createDatabaseName.value.trim();
|
|
if (!name || !node.connectionId) return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
if (config?.db_type === "mongodb") {
|
|
const plan: AuthorizationPlan = {
|
|
steps: [
|
|
{
|
|
id: "create-database",
|
|
label: `create database ${name}`,
|
|
database: "",
|
|
sql: mongoCreateDatabasePreview(name),
|
|
operation: "createDatabase",
|
|
targetDatabase: name,
|
|
},
|
|
],
|
|
};
|
|
createDatabaseAuthorizationPlan.value = plan;
|
|
createDatabasePreviewSql.value = authorizationPlanSql(plan);
|
|
createDatabaseAuthorizationResults.value = [];
|
|
showCreateDatabaseDialog.value = false;
|
|
showCreateDatabasePreviewDialog.value = true;
|
|
return;
|
|
}
|
|
const sql = await buildCreateDatabaseSql({
|
|
databaseType: config?.db_type,
|
|
driverProfile: config?.driver_profile,
|
|
target: "database",
|
|
name,
|
|
charset: createDatabaseCharset.value,
|
|
collation: createDatabaseCollation.value,
|
|
});
|
|
const provider = resolveDatabaseUserAdminProviderForConnection(config);
|
|
const plan: AuthorizationPlan = provider
|
|
? buildCreateDatabaseAuthorizationPlan({ provider, database: name, createSql: sql, users: createDatabaseSelectedUsers.value })
|
|
: { steps: [{ id: "create-database", label: `create database ${name}`, database: "", sql, operation: "createDatabase", targetDatabase: name }] };
|
|
createDatabaseAuthorizationPlan.value = plan;
|
|
createDatabasePreviewSql.value = authorizationPlanSql(plan);
|
|
createDatabaseAuthorizationResults.value = [];
|
|
showCreateDatabaseDialog.value = false;
|
|
showCreateDatabasePreviewDialog.value = true;
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function applyCreateDatabaseAuthorizationPlan() {
|
|
const node = sidebarFormTarget.value ?? activeNode.value;
|
|
const plan = createDatabaseAuthorizationPlan.value;
|
|
const name = createDatabaseName.value.trim();
|
|
if (!node.connectionId || !plan || createDatabaseAuthorizationApplying.value) return;
|
|
createDatabaseAuthorizationApplying.value = true;
|
|
try {
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
const isMongoDatabaseCreation = config?.db_type === "mongodb";
|
|
const executePlan = () =>
|
|
executeAuthorizationPlan(plan, async (step) => {
|
|
if (isMongoDatabaseCreation && step.operation === "createDatabase") {
|
|
await api.mongoCreateDatabase(node.connectionId!, name);
|
|
return [];
|
|
}
|
|
return api.executeMulti(node.connectionId!, step.database, step.sql, undefined, undefined, { maxRows: 1000, continueOnError: true });
|
|
});
|
|
const results = isMongoDatabaseCreation
|
|
? await executeWithProductionContextGuard({
|
|
connection: config,
|
|
database: name,
|
|
reviewText: createDatabasePreviewSql.value,
|
|
source: t("production.sourceSidebar"),
|
|
execute: executePlan,
|
|
})
|
|
: await executeWithProductionSqlGuard({
|
|
connection: config,
|
|
database: "",
|
|
sql: createDatabasePreviewSql.value,
|
|
source: t("production.sourceSidebar"),
|
|
execute: executePlan,
|
|
});
|
|
if (!results) return;
|
|
const displayResults = results.map((result) => ({
|
|
...result,
|
|
message: result.message && result.step.operation === "createDatabase" ? appendCreateDatabaseErrorHint(config?.db_type, result.message, t) : result.message,
|
|
}));
|
|
createDatabaseAuthorizationResults.value = displayResults;
|
|
const created = displayResults.some((result) => result.step.id === "create-database" && result.status === "success");
|
|
const status = authorizationPlanStatus(displayResults);
|
|
if (created) {
|
|
await connectionStore.ensureVisibleDatabase(node.connectionId, name);
|
|
if (isMongoDatabaseCreation) {
|
|
await connectionStore.loadMongoDatabases(node.connectionId);
|
|
} else {
|
|
await connectionStore.loadDatabases(node.connectionId, { force: true });
|
|
}
|
|
}
|
|
toast(t(status === "success" ? "contextMenu.createDatabaseSuccess" : status === "partial" ? "contextMenu.createDatabasePartial" : "contextMenu.createDatabaseFailed", { name }), status === "success" ? 3000 : 5000);
|
|
} catch (error: any) {
|
|
const message = appendCreateDatabaseErrorHint(connectionStore.getConfig(node.connectionId)?.db_type, error?.message || String(error), t);
|
|
toast(t("contextMenu.tableOperationFailed", { message }), 8000);
|
|
} finally {
|
|
createDatabaseAuthorizationApplying.value = false;
|
|
}
|
|
}
|
|
|
|
function createDatabaseAuthorizationStepLabel(result: AuthorizationStepResult): string {
|
|
const step = result.step;
|
|
if (step.operation === "createDatabase") return t("contextMenu.createDatabaseStep", { database: step.targetDatabase });
|
|
if (step.operation === "grantDatabase") return t("contextMenu.createDatabaseGrantStep", { user: step.subject, database: step.targetDatabase });
|
|
if (step.operation === "grantCurrentObjects") {
|
|
return t("userAdmin.stepGrantCurrentObjects", {
|
|
user: step.subject,
|
|
database: step.targetDatabase,
|
|
schema: step.schema,
|
|
scope: createDatabaseAuthorizationScopeLabel(step.objectScope),
|
|
});
|
|
}
|
|
if (step.operation === "grantFutureObjects") {
|
|
return step.owner
|
|
? t("userAdmin.stepGrantFutureObjectsForOwner", { user: step.subject, database: step.targetDatabase, owner: step.owner, scope: createDatabaseAuthorizationScopeLabel(step.objectScope) })
|
|
: t("userAdmin.stepGrantFutureObjects", { user: step.subject, database: step.targetDatabase, scope: createDatabaseAuthorizationScopeLabel(step.objectScope) });
|
|
}
|
|
return step.label;
|
|
}
|
|
|
|
function createDatabaseAuthorizationScopeLabel(scope: AuthorizationStepResult["step"]["objectScope"]): string {
|
|
if (scope === "schemas") return t("userAdmin.scopeSchemas");
|
|
if (scope === "tables") return t("userAdmin.scopeTables");
|
|
if (scope === "sequences") return t("userAdmin.scopeSequences");
|
|
if (scope === "functions") return t("userAdmin.scopeFunctions");
|
|
return "";
|
|
}
|
|
|
|
function dropDatabase() {
|
|
void refreshDropDatabasePreviewSql();
|
|
dropDatabaseLoading.value = false;
|
|
showDropDatabaseConfirm.value = true;
|
|
}
|
|
|
|
async function confirmDropDatabase() {
|
|
const node = sidebarDangerTarget.value ?? activeNode.value;
|
|
if (node.type === "mongo-db") {
|
|
await confirmDropMongoDatabase();
|
|
return;
|
|
}
|
|
if (node.type === "vector-database") {
|
|
await confirmDropMilvusDatabase();
|
|
return;
|
|
}
|
|
|
|
const connectionId = node.connectionId;
|
|
if (!connectionId || dropDatabaseLoading.value) return;
|
|
dropDatabaseLoading.value = true;
|
|
try {
|
|
await connectionStore.ensureConnected(connectionId);
|
|
const sql =
|
|
dropDatabasePreviewSql.value ||
|
|
(await buildDropDatabaseSql({
|
|
databaseType: databaseTypeForNode(node),
|
|
name: node.label,
|
|
}));
|
|
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: "" });
|
|
if (!executed) return;
|
|
toast(t("contextMenu.dropDatabaseSuccess", { name: node.label }), 3000);
|
|
try {
|
|
await connectionStore.loadDatabases(connectionId, { force: true });
|
|
} catch {
|
|
// The database was already dropped successfully above; if this refresh
|
|
// itself fails (e.g. a network blip), evict the node directly instead
|
|
// of leaving the deleted database showing in the tree.
|
|
connectionStore.removeTreeNode(node.id);
|
|
}
|
|
showDropDatabaseConfirm.value = false;
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
} finally {
|
|
dropDatabaseLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function openCreateSchemaDialog() {
|
|
createSchemaName.value = "";
|
|
showCreateSchemaDialog.value = true;
|
|
}
|
|
|
|
async function confirmCreateSchema() {
|
|
const node = sidebarFormTarget.value ?? activeNode.value;
|
|
const name = createSchemaName.value.trim();
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
const isConnectionLevelSchemaCreation = node.type === "connection" && connectionNamespaceCreationTarget(config) === "schema";
|
|
const targetDatabase = isConnectionLevelSchemaCreation ? "" : node.database;
|
|
if (!name || !node.connectionId || (!targetDatabase && !isConnectionLevelSchemaCreation)) return;
|
|
showCreateSchemaDialog.value = false;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const sql = await buildCreateSchemaSql({
|
|
databaseType: effectiveDatabaseTypeForConnection(config),
|
|
name,
|
|
});
|
|
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: targetDatabase || "" });
|
|
toast(t("contextMenu.createSchemaSuccess", { name }), 3000);
|
|
if (isConnectionLevelSchemaCreation) {
|
|
await connectionStore.loadDatabases(node.connectionId, { force: true });
|
|
} else if (config?.db_type === "sqlserver") {
|
|
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, targetDatabase || "", { force: true });
|
|
} else {
|
|
await connectionStore.loadSchemas(node.connectionId, targetDatabase || "", { force: true });
|
|
}
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
function dropSchema() {
|
|
const node = sidebarDangerTarget.value ?? activeNode.value;
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
if (isXuguSyntheticTreeNode(config?.db_type, node.type, node.schema)) return;
|
|
void refreshDropSchemaPreviewSql();
|
|
showDropSchemaConfirm.value = true;
|
|
}
|
|
|
|
async function confirmDropSchema() {
|
|
const node = sidebarDangerTarget.value ?? activeNode.value;
|
|
if (!node.connectionId || !node.database) return;
|
|
try {
|
|
const config = connectionStore.getConfig(node.connectionId);
|
|
if (isXuguSyntheticTreeNode(config?.db_type, node.type, node.schema)) return;
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const dbType = effectiveDatabaseTypeForConnection(config);
|
|
let dropExecutionSchema: string | undefined;
|
|
if (dbType === "dameng") {
|
|
dropExecutionSchema = damengDropSchemaExecutionSchema(config?.username, node.label) ?? undefined;
|
|
if (!dropExecutionSchema) throw new Error(t("contextMenu.dropDamengSchemaRequiresDifferentDba"));
|
|
}
|
|
const sql =
|
|
dropSchemaPreviewSql.value ||
|
|
(await buildDropSchemaSql({
|
|
databaseType: databaseTypeForNode(node),
|
|
name: node.label,
|
|
}));
|
|
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: dropExecutionSchema });
|
|
toast(t("contextMenu.dropSchemaSuccess", { name: node.label }), 3000);
|
|
if (config?.db_type === "sqlserver") {
|
|
await connectionStore.loadSqlServerDatabaseObjects(node.connectionId, node.database, { force: true });
|
|
} else if (isSingleDatabase(dbType)) {
|
|
await connectionStore.loadDatabases(node.connectionId, { force: true });
|
|
} else {
|
|
await connectionStore.loadSchemas(node.connectionId, node.database, { force: true });
|
|
}
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
function duplicateStructure(source: TreeNode = activeNode.value) {
|
|
if (!isDuplicateStructureSource(source)) return;
|
|
if (databaseTypeForNode(source) === "victoriametrics") return;
|
|
duplicateStructureSource.value = source;
|
|
duplicateTableName.value = `${source.label}_copy`;
|
|
showDuplicateDialog.value = true;
|
|
}
|
|
|
|
function isDuplicateStructureSource(node: TreeNode): node is DuplicateStructureSource {
|
|
return node.type === "table" && !!node.connectionId && !!node.database;
|
|
}
|
|
|
|
async function confirmDuplicateStructure() {
|
|
const node = duplicateStructureSource.value || (isDuplicateStructureSource(activeNode.value) ? activeNode.value : null);
|
|
const newName = duplicateTableName.value.trim();
|
|
if (!newName || !node) return;
|
|
if (databaseTypeForNode(node) === "victoriametrics") return;
|
|
showDuplicateDialog.value = false;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
const databaseType = databaseTypeForNode(node);
|
|
const plan = await buildDuplicateTableStructurePlan({
|
|
connectionId: node.connectionId,
|
|
database: node.database,
|
|
catalog: node.catalog,
|
|
databaseType,
|
|
schema: node.schema,
|
|
sourceName: node.label,
|
|
targetName: newName,
|
|
tableComment: node.comment,
|
|
identifierQuote: connectionStore.connectionIdentifierQuote?.(node.connectionId),
|
|
});
|
|
await executeTreeNodeSqlWithProductionGuard(node, plan.sql, {
|
|
database: node.database,
|
|
schema: node.schema,
|
|
executeAsScript: plan.executeAsScript,
|
|
});
|
|
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }), 3000);
|
|
await refreshTableList(node);
|
|
} catch (e: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
|
}
|
|
}
|
|
|
|
async function confirmPasteTable() {
|
|
const entries = pasteTableEntries.value.filter((entry) => entry.targetName.trim());
|
|
if (entries.length === 0) return;
|
|
const clipboardAtPasteStart = connectionStore.treeClipboard;
|
|
const mode = pasteTableMode.value;
|
|
const copyData = pasteTableModeCopiesData(mode) && pasteTableDataCopySupported.value;
|
|
showPasteDialog.value = false;
|
|
let successCount = 0;
|
|
let pasteFailCount = 0;
|
|
let firstPasteError: unknown;
|
|
let refreshFailCount = 0;
|
|
let refreshError: unknown;
|
|
let pasteCancelled = false;
|
|
let hasMutatedTable = false;
|
|
const refreshTargets = new Map<string, { connectionId: string; database: string; schema?: string }>();
|
|
const queueRefreshTarget = (entry: (typeof entries)[number]) => {
|
|
const refreshKey = `${entry.connectionId}:${entry.database}:${entry.schema || ""}`;
|
|
refreshTargets.set(refreshKey, {
|
|
connectionId: entry.connectionId,
|
|
database: entry.database,
|
|
schema: entry.schema,
|
|
});
|
|
};
|
|
for (const entry of entries) {
|
|
const targetName = entry.targetName.trim();
|
|
try {
|
|
await connectionStore.ensureConnected(entry.connectionId);
|
|
const databaseType = entry.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(entry.connectionId)) : undefined;
|
|
let sourceColumns: ColumnInfo[] | undefined;
|
|
if (mode === "structure-and-data" || mode === "structure-only") {
|
|
const plan = await buildDuplicateTableStructurePlan({
|
|
connectionId: entry.connectionId,
|
|
database: entry.database,
|
|
databaseType,
|
|
schema: entry.schema,
|
|
sourceName: entry.sourceName,
|
|
targetName,
|
|
tableComment: entry.tableComment,
|
|
identifierQuote: connectionStore.connectionIdentifierQuote?.(entry.connectionId),
|
|
});
|
|
sourceColumns = plan.sourceColumns;
|
|
const structureExecuted = await executeTreeNodeSqlWithProductionGuard(entry, plan.sql, {
|
|
database: entry.database,
|
|
schema: entry.schema,
|
|
executeAsScript: plan.executeAsScript,
|
|
});
|
|
if (!structureExecuted) {
|
|
pasteCancelled = true;
|
|
break;
|
|
}
|
|
hasMutatedTable = true;
|
|
queueRefreshTarget(entry);
|
|
}
|
|
if (copyData) {
|
|
if (!sourceColumns) {
|
|
sourceColumns = await api.getColumns(entry.connectionId, entry.database, entry.schema || "", entry.sourceName);
|
|
}
|
|
const dataCopyColumnOptions = tableDataCopyColumnOptions(databaseType, sourceColumns);
|
|
if (dataCopyColumnOptions.columns.length === 0) {
|
|
throw new Error("No writable columns available for table data copy.");
|
|
}
|
|
const dataSql = await buildCopyTableDataSql({
|
|
databaseType,
|
|
schema: entry.schema,
|
|
sourceName: entry.sourceName,
|
|
targetName,
|
|
normalizeNewTargetName: mode === "structure-and-data",
|
|
identifierQuote: connectionStore.connectionIdentifierQuote?.(entry.connectionId),
|
|
...dataCopyColumnOptions,
|
|
});
|
|
const dataExecuted = await executeTreeNodeSqlWithProductionGuard(entry, dataSql, { database: entry.database, schema: entry.schema });
|
|
if (!dataExecuted) {
|
|
pasteCancelled = true;
|
|
break;
|
|
}
|
|
hasMutatedTable = true;
|
|
queueRefreshTarget(entry);
|
|
}
|
|
successCount++;
|
|
} catch (e: any) {
|
|
pasteFailCount++;
|
|
firstPasteError ??= e;
|
|
console.error(`Failed to paste table "${entry.sourceName}" -> "${targetName}":`, e);
|
|
}
|
|
}
|
|
for (const refreshTarget of refreshTargets.values()) {
|
|
try {
|
|
await connectionStore.refreshObjectListTreeNode(refreshTarget.connectionId, refreshTarget.database, refreshTarget.schema);
|
|
} catch (e: any) {
|
|
refreshFailCount++;
|
|
refreshError ??= e;
|
|
console.error(`Failed to refresh pasted tables for "${refreshTarget.database}"${refreshTarget.schema ? ` schema "${refreshTarget.schema}"` : ""}:`, e);
|
|
}
|
|
}
|
|
if (pasteCancelled) {
|
|
if (hasMutatedTable && refreshFailCount === 0) {
|
|
toast(t("contextMenu.pasteTableCancelledAfterPartial"), 5000);
|
|
}
|
|
if (refreshFailCount > 0) {
|
|
const refreshMessage = refreshError instanceof Error ? refreshError.message : String(refreshError);
|
|
toast(t("contextMenu.pasteTableRefreshFailed", { message: translateBackendError(t, refreshMessage) }), 5000);
|
|
}
|
|
return;
|
|
}
|
|
const pasteFeedback = tablePasteFeedback(successCount, pasteFailCount, firstPasteError);
|
|
if (pasteFailCount === 0) {
|
|
if (connectionStore.treeClipboard === clipboardAtPasteStart) {
|
|
connectionStore.treeClipboard = null;
|
|
}
|
|
toast(t("contextMenu.batchPasteSuccess", { count: successCount }), 3000);
|
|
} else {
|
|
toast(`${t("contextMenu.batchPastePartialFail", { success: pasteFeedback.successCount, failed: pasteFeedback.failedCount })}\n${t("contextMenu.tableOperationFailed", { message: translateBackendError(t, pasteFeedback.firstError) })}`, 5000);
|
|
}
|
|
if (refreshFailCount > 0) {
|
|
const refreshMessage = refreshError instanceof Error ? refreshError.message : String(refreshError);
|
|
toast(t("contextMenu.pasteTableRefreshFailed", { message: translateBackendError(t, refreshMessage) }), 5000);
|
|
}
|
|
}
|
|
|
|
function openPasteTableDialog() {
|
|
const clipboard = connectionStore.treeClipboard;
|
|
if (clipboard?.kind !== "table-copy") {
|
|
toast(t("contextMenu.noTableToPaste"), 2000);
|
|
return;
|
|
}
|
|
if (canTransferTreeClipboardToCurrentNode()) {
|
|
openTransferFromTreeClipboard();
|
|
return;
|
|
}
|
|
if (!canPasteTreeClipboardToCurrentNode()) {
|
|
toast(t("contextMenu.noTableToPaste"), 2000);
|
|
return;
|
|
}
|
|
pasteTableMode.value = defaultPasteTableMode(currentDatabaseType());
|
|
pasteTableEntries.value = clipboard.tables.map((entry) => ({
|
|
sourceName: entry.tableName,
|
|
targetName: `${entry.tableName}_copy`,
|
|
connectionId: entry.connectionId,
|
|
database: entry.database,
|
|
schema: normalizeTreeClipboardSchema(entry.connectionId, entry.database, entry.schema),
|
|
tableComment: entry.tableComment,
|
|
}));
|
|
showPasteDialog.value = true;
|
|
}
|
|
|
|
function createTable() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId || !node.database) return;
|
|
if (connectionStore.getConfig(node.connectionId)?.db_type === "hbase") {
|
|
const tabId = queryStore.createTab(node.connectionId, node.database, node.database, "hbase", undefined, "");
|
|
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId);
|
|
if (tab) tab.hbaseCreateTableOnOpen = true;
|
|
return;
|
|
}
|
|
queryStore.openTableStructure(node.connectionId, node.database, node.schema, "");
|
|
}
|
|
|
|
function createView() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId || !node.database) return;
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
const viewName = "new_view";
|
|
const effectiveDbType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId));
|
|
// Quote the schema/name with the driver-reported identifier quote so
|
|
// hyphenated schemas (Kingbase MySQL compat) render as valid identifiers.
|
|
const viewSqlName =
|
|
effectiveDbType === "informix" || !node.schema
|
|
? viewName
|
|
: qualifiedTableName({
|
|
databaseType: effectiveDbType,
|
|
identifierQuote: connectionStore.connectionIdentifierQuote?.(node.connectionId),
|
|
schema: node.schema,
|
|
tableName: viewName,
|
|
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
|
quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers,
|
|
});
|
|
const tabId = queryStore.createTab(node.connectionId, node.database, t("contextMenu.createView"), "query", node.schema, undefined, node.catalog);
|
|
queryStore.updateSql(tabId, `CREATE VIEW ${viewSqlName} AS\nSELECT\n *\nFROM table_name;\n`);
|
|
queryStore.setObjectSource(tabId, {
|
|
schema: node.schema,
|
|
name: viewName,
|
|
objectType: "VIEW",
|
|
});
|
|
}
|
|
|
|
function requestDeleteHBaseTable() {
|
|
showHBaseDeleteTableConfirm.value = true;
|
|
}
|
|
|
|
async function confirmDeleteHBaseTable() {
|
|
const node = sidebarDangerTarget.value ?? activeNode.value;
|
|
if (!node.connectionId || !node.database || connectionStore.getConfig(node.connectionId)?.db_type !== "hbase") return;
|
|
try {
|
|
await connectionStore.ensureConnected(node.connectionId);
|
|
await api.hbaseDeleteTable(node.connectionId, node.database, node.label);
|
|
closeDroppedTableObjectTabsForNode(node);
|
|
connectionStore.removePinnedTreeNodes([node]);
|
|
await connectionStore.refreshObjectListTreeNode(node.connectionId, node.database);
|
|
toast(t("hbase.tableDeleted", { table: node.database === "default" ? node.label : `${node.database}:${node.label}` }));
|
|
} catch (error: any) {
|
|
toast(t("contextMenu.tableOperationFailed", { message: error?.message || String(error) }), 5000);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function createMysqlObjectTemplate() {
|
|
const node = activeNode.value;
|
|
if (!node.connectionId || !node.database) return;
|
|
const template = mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node);
|
|
if (!template) return;
|
|
connectionStore.activeConnectionId = node.connectionId;
|
|
const tabId = queryStore.createTab(node.connectionId, node.database, t(template.titleKey), "query", node.schema, undefined, node.catalog);
|
|
queryStore.updateSql(tabId, template.sql);
|
|
}
|
|
|
|
const canExpand = computed(() => {
|
|
if (activeNode.value.type === "type" && customTypeCapabilities(currentDatabaseType()).details) {
|
|
return activeNode.value.hasMembers === true;
|
|
}
|
|
return canTreeNodeShowExpander({
|
|
type: activeNode.value.type,
|
|
childCount: activeNode.value.children?.length ?? 0,
|
|
explicitContainer: activeNode.value.xuguTypeMembersExpandable === true,
|
|
});
|
|
});
|
|
|
|
const canPin = computed(() => canTreeNodePin(activeNode.value.type));
|
|
|
|
const canOpenSqlFileExecution = computed(() => {
|
|
return supportsSqlFileExecution(rawDatabaseType());
|
|
});
|
|
|
|
const canExportAllDatabases = computed(() => {
|
|
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return false;
|
|
const dbType = connectionStore.getConfig(activeNode.value.connectionId)?.db_type;
|
|
return !["redis", "mongodb", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "plugin"].includes(dbType || "");
|
|
});
|
|
|
|
const canOpenScheduledBackups = computed(() => {
|
|
// Match the backup page/engine, which filter and key off the raw db_type
|
|
// (effectiveDatabaseTypeForConnection maps gbase/mysql-dialect jdbc to "mysql",
|
|
// which would show an entry the backup dialog then can't list).
|
|
return isTauriRuntime() && supportsScheduledDatabaseBackup(rawDatabaseType());
|
|
});
|
|
|
|
const canOpenDiagram = computed(() => {
|
|
return !!activeNode.value.database && supportsSchemaDiagram(currentDatabaseType());
|
|
});
|
|
|
|
const canOpenDatabaseSearch = computed(() => {
|
|
return !!activeNode.value.database && supportsDatabaseSearch(currentDatabaseType());
|
|
});
|
|
|
|
const canOpenObjectBrowser = computed(() => {
|
|
return supportsObjectBrowserTreeNode(rawDatabaseType(), activeNode.value.type);
|
|
});
|
|
|
|
const canOpenConnectionDatabaseBrowser = computed(() => {
|
|
return activeNode.value.type === "connection" && supportsConnectionDatabaseBrowser(rawDatabaseType());
|
|
});
|
|
|
|
const canOpenTableImport = computed(() => {
|
|
const node = activeNode.value;
|
|
const supportedNode = node.type === "table" || ((node.type === "database" || node.type === "schema" || node.type === "group-tables") && canCreateTable.value);
|
|
return supportedNode && !isSqlServerLinkedNode(node) && !!node.connectionId && !!node.database && supportsTableImport(currentDatabaseType());
|
|
});
|
|
|
|
const canOpenStructureEditor = computed(() => {
|
|
const editableNode = activeNode.value.type === "table" || ((activeNode.value.type === "column" || activeNode.value.type === "index") && !!activeNode.value.tableName);
|
|
return editableNode && !isSqlServerLinkedNode(activeNode.value) && !!activeNode.value.connectionId && !!activeNode.value.database && supportsTableStructureEditing(currentTableStructureDatabaseType());
|
|
});
|
|
|
|
const canOpenFieldLineage = computed(() => {
|
|
return activeNode.value.type === "column" && !!activeNode.value.database && !!activeNode.value.tableName && supportsFieldLineage(currentDatabaseType());
|
|
});
|
|
|
|
const hasTypeMenu = computed(() => {
|
|
const t = activeNode.value.type;
|
|
return t === "connection" || t === "database" || t === "schema" || t === "table" || t === "view" || t === "column" || t === "procedure" || t === "function" || t === "trigger" || t === "package" || t === "package-body" || t === "type" || t === "type-body" || isGroupLabel(activeNode.value);
|
|
});
|
|
|
|
const isSelected = computed(() => connectionStore.selectedTreeNodeId === activeNode.value.id);
|
|
|
|
const isMultiSelected = computed(() => connectionStore.selectedTreeNodeIdsSet.has(activeNode.value.id));
|
|
|
|
const dangerDialogRoutes: Array<{ flag: { value: boolean }; createRequest: () => SidebarDangerDialogRequest }> = [];
|
|
const showHBaseDeleteTableConfirm = shallowRef(false);
|
|
const showDeleteSavedSqlConfirm = shallowRef(false);
|
|
|
|
let stopDangerDialogRouting: (() => void) | null = null;
|
|
|
|
function routeDangerDialog(flag: { value: boolean }, createRequest: () => SidebarDangerDialogRequest) {
|
|
dangerDialogRoutes.push({ flag, createRequest });
|
|
}
|
|
|
|
function ensureDangerDialogRouting() {
|
|
if (stopDangerDialogRouting) return;
|
|
stopDangerDialogRouting = watch(
|
|
dangerDialogRoutes.map((route) => route.flag),
|
|
(openFlags) => {
|
|
if (sidebarTreeDialogOwner.value !== treeItemDialogOwner) return;
|
|
openFlags.forEach((open, index) => {
|
|
if (!open) return;
|
|
const route = dangerDialogRoutes[index];
|
|
route.flag.value = false;
|
|
emit("open-danger-dialog", route.createRequest());
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
function dangerRequest(request: Omit<SidebarDangerDialogRequest, "target">): SidebarDangerDialogRequest {
|
|
const target = createSidebarActionTarget(activeNode.value);
|
|
const routedRequest = request as SidebarDangerDialogRequest;
|
|
const confirm = request.confirm;
|
|
routedRequest.confirm = async () => {
|
|
activateActionTarget(target);
|
|
return confirm();
|
|
};
|
|
if (request.option?.onChange) {
|
|
const onChange = request.option.onChange;
|
|
request.option.onChange = async (checked) => {
|
|
activateActionTarget(target);
|
|
await onChange(checked);
|
|
};
|
|
}
|
|
for (const option of request.options ?? []) {
|
|
if (!option.onChange) continue;
|
|
const onChange = option.onChange;
|
|
option.onChange = async (checked) => {
|
|
activateActionTarget(target);
|
|
await onChange(checked);
|
|
};
|
|
}
|
|
if (request.textInput?.onInput) {
|
|
const onInput = request.textInput.onInput;
|
|
request.textInput.onInput = async (value) => {
|
|
activateActionTarget(target);
|
|
await onInput(value);
|
|
};
|
|
}
|
|
routedRequest.target = target;
|
|
sidebarDangerTarget.value = routedRequest.target;
|
|
return routedRequest;
|
|
}
|
|
|
|
routeDangerDialog(showDropTableConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.confirmDropTableTitle"),
|
|
message: t("contextMenu.confirmDropTableMessage", { name: activeNode.value.label }),
|
|
get sql() {
|
|
return dropTablePreviewSql.value;
|
|
},
|
|
confirmLabel: t("contextMenu.dropTable"),
|
|
option: canDropTableCascade.value
|
|
? {
|
|
checked: dropTableCascade.value,
|
|
label: t("contextMenu.dropTableCascade"),
|
|
hint: t("contextMenu.dropTableCascadeHint"),
|
|
async onChange(checked) {
|
|
dropTableCascade.value = checked;
|
|
await refreshDropTablePreviewSql();
|
|
},
|
|
}
|
|
: undefined,
|
|
closeOnConfirm: false,
|
|
cancelRunning: () => sidebarDangerRunningCancel.value?.(),
|
|
confirm: confirmDropTable,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showHBaseDeleteTableConfirm, () => {
|
|
const table = activeNode.value.database && activeNode.value.database !== "default" ? `${activeNode.value.database}:${activeNode.value.label}` : activeNode.value.label;
|
|
return dangerRequest({
|
|
title: t("hbase.deleteTable"),
|
|
message: t("hbase.deleteTableConfirm", { table }),
|
|
details: table,
|
|
confirmLabel: t("common.delete"),
|
|
confirm: confirmDeleteHBaseTable,
|
|
});
|
|
});
|
|
|
|
routeDangerDialog(showDeleteSavedSqlConfirm, () =>
|
|
dangerRequest({
|
|
title: t("savedSql.deleteFile"),
|
|
message: t("savedSql.deleteFileConfirm", { name: activeNode.value.label }),
|
|
confirmLabel: t("dangerDialog.confirm"),
|
|
confirm: confirmDeleteSavedSqlFile,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showCopyConnectionSecretConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.copyPasswordConfirmTitle"),
|
|
message: t("contextMenu.copyPasswordConfirmMessage"),
|
|
confirmLabel: t("contextMenu.copyPasswordConfirmAction"),
|
|
confirm: () => {
|
|
const pending = pendingConnectionSecretCopy;
|
|
pendingConnectionSecretCopy = null;
|
|
if (pending) return performConnectionUrlCopy(pending.config, pending.format, pending.databaseOverride);
|
|
},
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showEmptyTableConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.confirmEmptyTableTitle"),
|
|
message: t("contextMenu.confirmEmptyTableMessage", { name: activeNode.value.label }),
|
|
get sql() {
|
|
return emptyTablePreviewSql.value;
|
|
},
|
|
confirmLabel: t("contextMenu.emptyTable"),
|
|
closeOnConfirm: false,
|
|
cancelRunning: () => sidebarDangerRunningCancel.value?.(),
|
|
confirm: confirmEmptyTable,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showBatchEmptyConfirm, () =>
|
|
dangerRequest({
|
|
title: batchEmptyConfirmTitle(),
|
|
message: batchEmptyConfirmMessage(),
|
|
get sql() {
|
|
return batchEmptyPreviewSql.value;
|
|
},
|
|
confirmLabel: batchEmptyConfirmLabel(),
|
|
confirm: confirmBatchEmpty,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showTruncateTableConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.confirmTruncateTableTitle"),
|
|
message: t("contextMenu.confirmTruncateTableMessage", { name: activeNode.value.label }),
|
|
get sql() {
|
|
return truncateTablePreviewSql.value;
|
|
},
|
|
confirmLabel: t("contextMenu.truncateTable"),
|
|
option: canTruncateTableCascade.value
|
|
? {
|
|
checked: truncateTableCascade.value,
|
|
label: t("contextMenu.truncateTableCascade"),
|
|
hint: t("contextMenu.truncateTableCascadeHint"),
|
|
async onChange(checked) {
|
|
truncateTableCascade.value = checked;
|
|
await refreshTruncateTablePreviewSql();
|
|
},
|
|
}
|
|
: undefined,
|
|
closeOnConfirm: false,
|
|
cancelRunning: () => sidebarDangerRunningCancel.value?.(),
|
|
confirm: confirmTruncateTable,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showVacuumTableConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.vacuumTableTitle"),
|
|
message: t("contextMenu.vacuumTableMessage", { name: activeNode.value.label }),
|
|
get detailsText() {
|
|
if (vacuumTableExecuting.value) return t("contextMenu.vacuumTableRunningHint");
|
|
return vacuumTableFull.value ? t("contextMenu.vacuumTableFullRisk") : vacuumTableAnalyze.value ? t("contextMenu.vacuumTableAnalyzeRisk") : t("contextMenu.vacuumTableDefaultRisk");
|
|
},
|
|
get sql() {
|
|
return vacuumTablePreviewSql.value;
|
|
},
|
|
get confirmLabel() {
|
|
return vacuumTableExecuting.value ? t("contextMenu.vacuumTableRunning") : vacuumTableFull.value ? t("contextMenu.vacuumTableFullConfirm") : t("contextMenu.vacuumTable");
|
|
},
|
|
loading: false,
|
|
closeOnConfirm: false,
|
|
options: [
|
|
{
|
|
checked: vacuumTableAnalyze.value,
|
|
label: t("contextMenu.vacuumTableAnalyze"),
|
|
hint: t("contextMenu.vacuumTableAnalyzeHint"),
|
|
compact: true,
|
|
async onChange(checked) {
|
|
vacuumTableAnalyze.value = checked;
|
|
await refreshVacuumPreviewForOptions();
|
|
},
|
|
},
|
|
{
|
|
checked: vacuumTableFull.value,
|
|
label: t("contextMenu.vacuumTableFull"),
|
|
hint: t("contextMenu.vacuumTableFullHint"),
|
|
compact: true,
|
|
danger: true,
|
|
async onChange(checked) {
|
|
vacuumTableFull.value = checked;
|
|
await refreshVacuumPreviewForOptions();
|
|
},
|
|
},
|
|
],
|
|
confirm: confirmVacuumTable,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showMysqlAutoIncrementConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.mysqlAutoIncrementTitle"),
|
|
message: t("contextMenu.mysqlAutoIncrementMessage", { name: activeNode.value.label }),
|
|
detailsText: t("contextMenu.mysqlAutoIncrementNonemptyHint"),
|
|
get sql() {
|
|
return mysqlAutoIncrementPreviewSql.value;
|
|
},
|
|
confirmLabel: t("contextMenu.mysqlAutoIncrement"),
|
|
textInput: {
|
|
value: mysqlAutoIncrementValue.value,
|
|
label: t("contextMenu.mysqlAutoIncrementValue"),
|
|
placeholder: "1",
|
|
inputMode: "numeric",
|
|
async onInput(value) {
|
|
mysqlAutoIncrementValue.value = value;
|
|
if (sidebarDangerTarget.value) activateActionTarget(sidebarDangerTarget.value);
|
|
await refreshMysqlAutoIncrementPreviewSql();
|
|
},
|
|
},
|
|
confirm: confirmMysqlAutoIncrement,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showBatchMysqlAutoIncrementConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.batchMysqlAutoIncrementTitle", { count: batchMysqlAutoIncrementTargets.value.length }),
|
|
message: t("contextMenu.batchMysqlAutoIncrementMessage", { count: batchMysqlAutoIncrementTargets.value.length }),
|
|
detailsText: t("contextMenu.mysqlAutoIncrementNonemptyHint"),
|
|
get sql() {
|
|
return batchMysqlAutoIncrementPreviewSql.value;
|
|
},
|
|
get confirmLabel() {
|
|
return t("contextMenu.batchMysqlAutoIncrement", { count: batchMysqlAutoIncrementTargets.value.length });
|
|
},
|
|
textInput: {
|
|
value: mysqlAutoIncrementValue.value,
|
|
label: t("contextMenu.mysqlAutoIncrementValue"),
|
|
placeholder: "1",
|
|
inputMode: "numeric",
|
|
async onInput(value) {
|
|
mysqlAutoIncrementValue.value = value;
|
|
await refreshBatchMysqlAutoIncrementPreviewSql();
|
|
},
|
|
},
|
|
confirm: confirmBatchMysqlAutoIncrement,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showDropObjectConfirm, () =>
|
|
dangerRequest({
|
|
title: dropObjectConfirmTitle(),
|
|
message: dropObjectConfirmMessage(),
|
|
get sql() {
|
|
return dropObjectPreviewSql.value;
|
|
},
|
|
confirmLabel: dropObjectMenuLabel(),
|
|
confirm: confirmDropObject,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showDropTableChildObjectConfirm, () =>
|
|
dangerRequest({
|
|
title: dropTableChildObjectConfirmTitle(),
|
|
message: dropTableChildObjectConfirmMessage(),
|
|
get sql() {
|
|
return dropTableChildObjectPreviewSql.value;
|
|
},
|
|
confirmLabel: dropTableChildObjectMenuLabel(),
|
|
confirm: confirmDropTableChildObject,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showBatchDropConfirm, () =>
|
|
dangerRequest({
|
|
title: batchDropConfirmTitle(),
|
|
message: batchDropConfirmMessage(),
|
|
get sql() {
|
|
return batchDropPreviewSql.value;
|
|
},
|
|
confirmLabel: batchDropMenuLabel(),
|
|
get progress() {
|
|
return batchDropProgress.value.total > 0 ? batchDropProgress.value : undefined;
|
|
},
|
|
option: canBatchDropCascade.value
|
|
? {
|
|
checked: batchDropCascade.value,
|
|
label: t("contextMenu.dropTableCascade"),
|
|
hint: t("contextMenu.dropTableCascadeHint"),
|
|
async onChange(checked) {
|
|
batchDropCascade.value = checked;
|
|
await refreshBatchDropPreviewSql();
|
|
},
|
|
}
|
|
: undefined,
|
|
confirm: confirmBatchDrop,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showBatchTruncateConfirm, () =>
|
|
dangerRequest({
|
|
title: batchTruncateConfirmTitle(),
|
|
message: batchTruncateConfirmMessage(),
|
|
get sql() {
|
|
return batchTruncatePreviewSql.value;
|
|
},
|
|
confirmLabel: batchTruncateMenuLabel(),
|
|
option: canBatchTruncateCascade.value
|
|
? {
|
|
checked: batchTruncateCascade.value,
|
|
label: t("contextMenu.truncateTableCascade"),
|
|
hint: t("contextMenu.truncateTableCascadeHint"),
|
|
async onChange(checked) {
|
|
batchTruncateCascade.value = checked;
|
|
await refreshBatchTruncatePreviewSql();
|
|
},
|
|
}
|
|
: undefined,
|
|
confirm: confirmBatchTruncate,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showDropDatabaseConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.confirmDropDatabaseTitle"),
|
|
message: t("contextMenu.confirmDropDatabaseMessage", { name: activeNode.value.label }),
|
|
get sql() {
|
|
return dropDatabasePreviewSql.value;
|
|
},
|
|
confirmLabel: t("contextMenu.dropDatabase"),
|
|
get loading() {
|
|
return dropDatabaseLoading.value;
|
|
},
|
|
closeOnConfirm: false,
|
|
confirm: confirmDropDatabase,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showDropMongoCollectionConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.confirmDropCollectionTitle"),
|
|
message: t("contextMenu.confirmDropCollectionMessage", { name: activeNode.value.label }),
|
|
confirmLabel: t("contextMenu.dropCollection"),
|
|
get loading() {
|
|
return dropMongoCollectionLoading.value;
|
|
},
|
|
closeOnConfirm: false,
|
|
confirm: () => (activeNode.value.type === "vector-collection" ? confirmDropMilvusCollection() : confirmDropMongoCollection()),
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showDropMongoIndexConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.confirmDropIndexTitle"),
|
|
message: t("contextMenu.confirmDropMongoIndexMessage", { name: mongoIndexNameForNode(activeNode.value), collection: activeNode.value.tableName || "" }),
|
|
details: mongoIndexDropPreview(activeNode.value, mongoIndexNameForNode(activeNode.value)),
|
|
confirmLabel: t("contextMenu.dropIndex"),
|
|
get loading() {
|
|
return dropMongoIndexLoading.value;
|
|
},
|
|
closeOnConfirm: false,
|
|
confirm: confirmDropMongoIndex,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showDropAllMongoIndexesConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.dropAllIndexes"),
|
|
message: t("contextMenu.confirmDropMongoAllIndexesMessage", { name: activeNode.value.tableName || activeNode.value.label }),
|
|
detailsText: t("contextMenu.confirmDropMongoAllIndexesDetails"),
|
|
sql: mongoDropAllIndexesPreview(activeNode.value),
|
|
confirmLabel: t("contextMenu.dropAllIndexes"),
|
|
get loading() {
|
|
return dropAllMongoIndexesLoading.value;
|
|
},
|
|
closeOnConfirm: false,
|
|
confirm: confirmDropAllMongoIndexes,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showClearElasticsearchIndexConfirm, () => {
|
|
// Pin the label for the life of this dialog. `activeNode` follows the tree
|
|
// selection, which the confirmation must not, and the typed-name gate below
|
|
// has to compare against the index the operator actually opened.
|
|
const index = activeNode.value.label;
|
|
const isPattern = isElasticsearchIndexPattern(index);
|
|
clearElasticsearchIndexTypedName.value = "";
|
|
return dangerRequest({
|
|
title: t("contextMenu.elasticsearchClearIndex"),
|
|
// A grouped node is a wildcard covering many indexes, so it gets its own
|
|
// wording rather than one that reads as a single index.
|
|
message: t(isPattern ? "contextMenu.elasticsearchClearIndexPatternMessage" : "contextMenu.elasticsearchClearIndexMessage", { index }),
|
|
detailsText: t("contextMenu.elasticsearchClearIndexDetails"),
|
|
sql: elasticsearchClearIndexPreview(index),
|
|
confirmLabel: t("contextMenu.elasticsearchClearIndexConfirm"),
|
|
get loading() {
|
|
return clearElasticsearchIndexLoading.value;
|
|
},
|
|
// A wildcard node clears every index it matches, so the pattern has to be
|
|
// typed back before the button unlocks. Concrete index names skip this.
|
|
...(isPattern
|
|
? {
|
|
textInput: {
|
|
value: "",
|
|
label: t("contextMenu.elasticsearchClearIndexTypeToConfirm", { index }),
|
|
placeholder: index,
|
|
onInput(value: string) {
|
|
clearElasticsearchIndexTypedName.value = value;
|
|
},
|
|
},
|
|
}
|
|
: {}),
|
|
// Declared directly on this literal, never inside the spread above: object
|
|
// spread evaluates an accessor once and copies the resulting value, which
|
|
// would freeze the gate shut at its construction-time answer. Concrete
|
|
// index names need no gate, and `isElasticsearchClearConfirmed` already
|
|
// returns true for them, so this stays unlocked for them.
|
|
get confirmDisabled() {
|
|
return !isElasticsearchClearConfirmed(index, clearElasticsearchIndexTypedName.value);
|
|
},
|
|
closeOnConfirm: false,
|
|
confirm: confirmClearElasticsearchIndex,
|
|
});
|
|
});
|
|
|
|
routeDangerDialog(showFlushRedisDbConfirm, () =>
|
|
dangerRequest({
|
|
title: t("redis.flushDb"),
|
|
message: t("redis.flushDbMessage"),
|
|
details: t("redis.flushDbDetails", { db: activeNode.value.database }),
|
|
confirmLabel: t("redis.flushDbConfirm"),
|
|
confirm: confirmFlushRedisDb,
|
|
}),
|
|
);
|
|
|
|
routeDangerDialog(showDropSchemaConfirm, () =>
|
|
dangerRequest({
|
|
title: t("contextMenu.confirmDropSchemaTitle"),
|
|
message: t("contextMenu.confirmDropSchemaMessage", { name: activeNode.value.label }),
|
|
get sql() {
|
|
return dropSchemaPreviewSql.value;
|
|
},
|
|
confirmLabel: t("contextMenu.dropSchema"),
|
|
confirm: confirmDropSchema,
|
|
}),
|
|
);
|
|
|
|
function moveToNewGroup() {
|
|
moveToNewGroupName.value = "";
|
|
showMoveToNewGroupDialog.value = true;
|
|
}
|
|
|
|
function confirmMoveToNewGroup() {
|
|
createGroupAndMoveConnection(moveToNewGroupName.value);
|
|
showMoveToNewGroupDialog.value = false;
|
|
}
|
|
|
|
let treeItemDialogController: Record<string, any> | null = null;
|
|
|
|
function connectionDialogCapabilities() {
|
|
return {
|
|
showDeleteConfirm,
|
|
connectionDeleteConfirmMessage,
|
|
confirmDelete,
|
|
connectionDeleteMenuLabel,
|
|
showMoveToNewGroupDialog,
|
|
moveToNewGroupName,
|
|
confirmMoveToNewGroup,
|
|
showDeleteGroupConfirm,
|
|
deleteConnectionsWithGroup,
|
|
connectionGroupDeleteConfirmMessage,
|
|
connectionGroupDeleteMenuLabel,
|
|
deletingConnectionGroups,
|
|
confirmDeleteGroup,
|
|
};
|
|
}
|
|
|
|
function objectDialogCapabilities() {
|
|
return {
|
|
showRenameObjectDialog,
|
|
renameObjectName,
|
|
renameObjectDialogTitle,
|
|
renameObjectPreviewSql,
|
|
renameObjectError,
|
|
confirmRenameObject,
|
|
showStructurePreviewDialog,
|
|
structurePreviewTitle,
|
|
isLoadingStructurePreview,
|
|
structurePreviewError,
|
|
structurePreviewSql,
|
|
copyStructurePreview,
|
|
saveStructurePreview,
|
|
showStructureDocCopyDialog,
|
|
structureDocCopyTitle,
|
|
structureDocCopyText,
|
|
selectTextareaContent,
|
|
copyStructureDocText,
|
|
showDuplicateDialog,
|
|
duplicateTableName,
|
|
confirmDuplicateStructure,
|
|
showPasteDialog,
|
|
pasteTableEntries,
|
|
pasteTableMode,
|
|
pasteTableDataCopySupported: pasteTableDataCopySupported.value,
|
|
confirmPasteTable,
|
|
};
|
|
}
|
|
|
|
function databaseDialogCapabilities() {
|
|
return {
|
|
showCreateDatabaseDialog,
|
|
createDatabaseName,
|
|
canSetCreateDatabaseCharset: canSetCreateDatabaseCharset.value || canSetCreateDatabaseLocale.value,
|
|
createDatabaseCharset,
|
|
createDatabaseCharsetOptions,
|
|
createDatabaseCharsetLoading,
|
|
normalizeCreateDatabaseCharset,
|
|
createDatabaseCollation,
|
|
createDatabaseUsers,
|
|
createDatabaseSelectedUsers,
|
|
createDatabaseUsersLoading,
|
|
createDatabaseUserKey,
|
|
createDatabaseUserLabel,
|
|
createDatabaseUserSelected,
|
|
toggleCreateDatabaseUser,
|
|
showCreateDatabasePreviewDialog,
|
|
createDatabasePreviewSql,
|
|
createDatabaseAuthorizationResults,
|
|
createDatabaseAuthorizationApplying,
|
|
applyCreateDatabaseAuthorizationPlan,
|
|
createDatabaseAuthorizationStepLabel,
|
|
createDatabaseCollationOptionsForCharset,
|
|
createDatabaseCollationsByCharset,
|
|
confirmCreateDatabase,
|
|
showEditDatabasePropertiesDialog,
|
|
updateCreateDatabaseCharset,
|
|
canEditDatabaseCharsetCollation: canEditDatabaseCharsetCollation.value,
|
|
updateEditDatabaseCharset,
|
|
editDatabasePropertiesLoading,
|
|
editDatabaseCharset,
|
|
editDatabaseCollation,
|
|
canEditDatabaseComment: canEditDatabaseComment.value,
|
|
editDatabaseCommentText,
|
|
editDatabasePropertiesPreviewSql,
|
|
confirmEditDatabaseProperties,
|
|
};
|
|
}
|
|
|
|
function databaseSpecificDialogCapabilities() {
|
|
return {
|
|
showCreateNacosNamespaceDialog,
|
|
createNacosNamespaceId,
|
|
createNacosNamespaceName,
|
|
createNacosNamespaceDesc,
|
|
createNacosNamespaceLoading,
|
|
confirmCreateNacosNamespace,
|
|
showEditNacosNamespaceDialog,
|
|
editNacosNamespaceName,
|
|
editNacosNamespaceDesc,
|
|
editNacosNamespaceLoading,
|
|
confirmEditNacosNamespace,
|
|
showDeleteNacosNamespaceConfirm,
|
|
deleteNacosNamespaceLoading,
|
|
confirmDeleteNacosNamespace,
|
|
showRenameMongoCollectionDialog,
|
|
renameMongoCollectionName,
|
|
renameMongoCollectionError,
|
|
renameMongoCollectionPreview,
|
|
renameMongoCollectionLoading,
|
|
confirmRenameMongoCollection,
|
|
showCloneMongoCollectionDialog,
|
|
cloneMongoCollectionName,
|
|
cloneMongoCollectionError,
|
|
cloneMongoCollectionLoading,
|
|
confirmCloneMongoCollection,
|
|
showCreateMongoIndexDialog,
|
|
showCreateMeilisearchIndexDialog,
|
|
meilisearchCreateIndexUid,
|
|
meilisearchCreateIndexPrimaryKey,
|
|
meilisearchCreateIndexError,
|
|
meilisearchCreateIndexLoading,
|
|
confirmCreateMeilisearchIndex,
|
|
mongoCreateIndexForm,
|
|
mongoCreateIndexFieldOptions,
|
|
mongoCreateIndexError,
|
|
mongoCreateIndexLoading,
|
|
mongoIndexKeyTypes,
|
|
mongoCreateIndexCanSubmit,
|
|
mongoCreateIndexCanAddField,
|
|
addMongoCreateIndexField,
|
|
removeMongoCreateIndexField,
|
|
confirmCreateMongoIndex,
|
|
showMongoIndexManagerDialog,
|
|
mongoIndexManagerRows,
|
|
mongoIndexManagerLoading,
|
|
mongoIndexManagerError,
|
|
mongoIndexManagerSelectedName,
|
|
mongoIndexManagerMode,
|
|
mongoIndexManagerSelected,
|
|
mongoIndexManagerCollectionName,
|
|
selectMongoIndexRow,
|
|
startCreateMongoIndexDraft,
|
|
startEditMongoIndexDraft,
|
|
cancelMongoIndexDraft,
|
|
dropSelectedMongoIndexRow,
|
|
canDropSelectedMongoIndexRow,
|
|
canEditSelectedMongoIndexRow,
|
|
confirmEditMongoIndex,
|
|
mongoEditIndexOriginalName,
|
|
loadMongoIndexManagerRows,
|
|
showRedisDatabaseAliasDialog,
|
|
redisDatabaseAliasInput,
|
|
redisDatabaseAliasSaving,
|
|
confirmRedisDatabaseAlias,
|
|
clearRedisDatabaseAlias,
|
|
showCreateSchemaDialog,
|
|
createSchemaName,
|
|
confirmCreateSchema,
|
|
showEditSchemaCommentDialog,
|
|
showCompileErrorDialog,
|
|
compileErrorTitle,
|
|
compileErrorMessage,
|
|
schemaCommentText,
|
|
schemaCommentLoading,
|
|
schemaCommentPreviewSql,
|
|
confirmEditSchemaComment,
|
|
};
|
|
}
|
|
|
|
function getTreeItemDialogController(): Record<string, any> {
|
|
if (treeItemDialogController) return treeItemDialogController;
|
|
treeItemDialogController = reactive<Record<string, any>>({
|
|
node: createSidebarActionTarget(activeNode.value),
|
|
t,
|
|
highlight,
|
|
...connectionDialogCapabilities(),
|
|
...objectDialogCapabilities(),
|
|
...databaseDialogCapabilities(),
|
|
...databaseSpecificDialogCapabilities(),
|
|
});
|
|
return treeItemDialogController;
|
|
}
|
|
|
|
const availableGroups = computed(() => connectionGroupDestinationRows(connectionStore.sidebarLayout));
|
|
|
|
onBeforeUnmount(() => {
|
|
stopDangerDialogRouting?.();
|
|
});
|
|
|
|
const shortcutCopyName = computed(() => settingsStore.editorSettings.shortcuts.copySidebarSelection);
|
|
|
|
const shortcutPaste = computed(() => settingsStore.editorSettings.shortcuts.pasteSidebarSelection);
|
|
|
|
const shortcutOpenDataInNewTab = computed(() => settingsStore.editorSettings.shortcuts.openDataInNewTab);
|
|
|
|
const shortcutEditConnection = computed(() => settingsStore.editorSettings.shortcuts.editSidebarConnection);
|
|
|
|
const shortcutRename = "F2";
|
|
|
|
const shortcutRefresh = "F5";
|
|
|
|
const shortcutDelete = "Delete";
|
|
|
|
function selectedDataExportTargetCount(): number {
|
|
return sidebarTableDataExportTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds).length;
|
|
}
|
|
|
|
function selectedAiTableTargets(): Array<TreeNode & { connectionId: string; database: string }> {
|
|
return resolveSidebarDdlTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds).filter((target) => target.type === "table");
|
|
}
|
|
|
|
function addToAiMenuItem(node: TreeNode): ContextMenuItem {
|
|
const tables = node.type === "table" ? selectedAiTableTargets() : [];
|
|
const count = tables.length;
|
|
return {
|
|
label: count > 1 ? t("contextMenu.addToAiMultiple", { count }) : t("contextMenu.addToAi"),
|
|
action: () => emit("add-to-ai", count > 1 ? tables : node),
|
|
icon: Sparkles,
|
|
};
|
|
}
|
|
|
|
function exportDataSubmenu(includeSqlInsert = true): ContextMenuItem {
|
|
const count = selectedDataExportTargetCount();
|
|
const children: ContextMenuItem[] = [
|
|
{ label: "CSV", action: () => exportData("csv") },
|
|
{ label: "JSON", action: () => exportData("json") },
|
|
];
|
|
if (includeSqlInsert) children.push({ label: "SQL INSERT", action: () => exportData("sql") });
|
|
children.push({ label: "XLSX", action: () => exportDataXlsx() });
|
|
return {
|
|
label: count > 1 ? t("contextMenu.exportDataMultiple", { count }) : t("contextMenu.exportData"),
|
|
icon: Upload,
|
|
children,
|
|
};
|
|
}
|
|
|
|
function copyStructureAsSubmenu(): ContextMenuItem {
|
|
return {
|
|
label: t("contextMenu.copyStructureAs"),
|
|
icon: Clipboard,
|
|
children: [
|
|
{ label: t("contextMenu.copyStructureAsTsv"), action: () => copyStructureAs("tsv") },
|
|
{ label: t("contextMenu.copyStructureAsMarkdown"), action: () => copyStructureAs("markdown") },
|
|
],
|
|
};
|
|
}
|
|
|
|
function moreActionsSubmenu(children: ContextMenuItem[]): ContextMenuItem {
|
|
return {
|
|
label: t("common.more"),
|
|
icon: ListTree,
|
|
variant: "destructive",
|
|
children,
|
|
};
|
|
}
|
|
|
|
function savedSqlHistoryScopeForNode(node: TreeNode): SavedSqlHistoryScope | null {
|
|
if (!node.connectionId) return null;
|
|
if (node.type === "connection") {
|
|
return { connectionId: node.connectionId };
|
|
}
|
|
if ((node.type === "database" || node.type === "schema") && hasTreeNodeDatabaseContext(node)) {
|
|
return {
|
|
connectionId: node.connectionId,
|
|
catalog: node.catalog ?? null,
|
|
database: node.database,
|
|
schema: node.type === "schema" ? node.schema : undefined,
|
|
};
|
|
}
|
|
if ((node.type === "table" || node.type === "view") && hasTreeNodeDatabaseContext(node)) {
|
|
return {
|
|
connectionId: node.connectionId,
|
|
catalog: node.catalog ?? null,
|
|
database: node.database,
|
|
schema: node.schema,
|
|
tableName: node.label,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function openSavedSqlHistoryFile(fileId: string) {
|
|
const file = await savedSqlStore.ensureFileContent(fileId);
|
|
if (!file) return;
|
|
const tabId = queryStore.openSavedSql(file);
|
|
connectionStore.activeConnectionId = queryStore.tabs.find((tab) => tab.id === tabId)?.connectionId ?? file.connectionId;
|
|
void savedSqlStore.recordFileUsage(file.id);
|
|
}
|
|
|
|
function savedSqlHistorySubmenu(): ContextMenuItem | null {
|
|
const scope = savedSqlHistoryScopeForNode(activeNode.value);
|
|
if (!scope) return null;
|
|
const files = rankSavedSqlHistory(savedSqlStore.allFiles, { ...scope, limit: 10 });
|
|
return {
|
|
label: t("contextMenu.sqlHistory"),
|
|
icon: ScrollText,
|
|
children:
|
|
files.length > 0
|
|
? files.map((file) => ({
|
|
label: file.name,
|
|
action: () => openSavedSqlHistoryFile(file.id),
|
|
icon: FileCode,
|
|
}))
|
|
: [
|
|
{
|
|
label: t("contextMenu.noSqlHistory"),
|
|
disabled: true,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
interface SidebarMenuFactoryContext {
|
|
node: TreeNode;
|
|
items: ContextMenuItem[];
|
|
deleteMenuLabel: (singleLabel: string) => string;
|
|
deleteMenuAction: (singleAction: () => void) => () => void;
|
|
truncateMenuLabel: (singleLabel: string) => string;
|
|
truncateMenuAction: (singleAction: () => void) => () => void;
|
|
emptyMenuLabel: (singleLabel: string) => string;
|
|
emptyMenuAction: (singleAction: () => void) => () => void;
|
|
batchAutoIncrementCount: number;
|
|
autoIncrementMenuLabel: (singleLabel: string) => string;
|
|
autoIncrementMenuAction: (singleAction: () => void) => () => void;
|
|
}
|
|
|
|
type SidebarMenuFactory = (context: SidebarMenuFactoryContext) => boolean;
|
|
|
|
function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|
const { node, items } = context;
|
|
// 2. Connection
|
|
if (node.type === "connection") {
|
|
if (isConnecting.value) {
|
|
items.push({ label: t("connection.cancelConnecting"), action: cancelConnectionAttempt, icon: X });
|
|
} else if (canDisconnectConnection()) {
|
|
items.push({ label: connectionDisconnectMenuLabel(), action: disconnectConnection, icon: Unplug });
|
|
// save_password=false 且本次运行期已输入密码:提供"断开并忘记本次密码",
|
|
// 清除会话凭据后下次连接需重新输入。
|
|
if (canForgetSessionCredential()) {
|
|
items.push({
|
|
label: t("connection.disconnectAndForgetPassword"),
|
|
action: disconnectAndForgetConnectionPassword,
|
|
icon: Unplug,
|
|
});
|
|
}
|
|
} else if (!isConnected.value) {
|
|
items.push({ label: t("contextMenu.openConnection"), action: toggle, icon: Plug });
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
const connectionUrlCopyMenu = connectionUrlCopyMenuItem();
|
|
if (connectionUrlCopyMenu) items.push(connectionUrlCopyMenu);
|
|
items.push({ label: "", separator: true });
|
|
const supportsQueryActions = supportsConnectionQueryActions(currentDatabaseType());
|
|
const supportsAiContext = supportsAiAssistantContext(currentDatabaseType());
|
|
if (supportsQueryActions) {
|
|
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
|
if (supportsAiContext) {
|
|
items.push(addToAiMenuItem(node));
|
|
}
|
|
}
|
|
if (canCreateMeilisearchIndex.value) {
|
|
items.push({ label: t("meilisearch.createIndex"), action: openCreateMeilisearchIndexDialog, icon: Plus });
|
|
}
|
|
const connectionWorkspace = node.connectionId ? driverProfileDatabaseWorkspace(connectionStore.getConfig(node.connectionId)?.driver_profile) : undefined;
|
|
if (connectionWorkspace?.entryScopes.includes("connection")) {
|
|
items.push({ label: t(connectionWorkspace.menuLabelKey), action: openProfileConnectionWorkspace, icon: GitBranch });
|
|
}
|
|
if (canOpenConnectionDatabaseBrowser.value) {
|
|
items.push({ label: t("contextMenu.openDatabaseBrowser"), action: openDatabaseBrowser, icon: TableProperties });
|
|
}
|
|
if (currentDatabaseType() === "redis") {
|
|
items.push({ label: t("contextMenu.instanceInfo"), action: openRedisInstanceInfo, icon: Info });
|
|
}
|
|
if (supportsQueryActions) {
|
|
const sqlHistoryMenu = savedSqlHistorySubmenu();
|
|
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
|
|
}
|
|
if (node.connectionId && connectionSupportsDatabaseUserAdmin(connectionStore.getConfig(node.connectionId))) {
|
|
items.push({ label: t("contextMenu.userAdmin"), action: openUserAdmin, icon: UsersRound });
|
|
}
|
|
if (node.connectionId && connectionSupportsProcessList(connectionStore.getConfig(node.connectionId))) {
|
|
items.push({ label: t("contextMenu.processList"), action: openProcessList, icon: Activity });
|
|
}
|
|
if (currentDatabaseType() === "sqlserver") {
|
|
items.push({ label: t("contextMenu.sqlServerTrace"), action: openSqlServerActivityTrace, icon: Activity });
|
|
}
|
|
if (
|
|
node.connectionId &&
|
|
(currentDatabaseType() === "nacos" || connectionSupportsXuguServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId)))
|
|
) {
|
|
items.push({ label: t("contextMenu.serverDashboard"), action: openServerDashboard, icon: Gauge });
|
|
}
|
|
if (currentDatabaseType() === "dameng") {
|
|
items.push({ label: t("contextMenu.damengUsers"), action: openDamengUsers, icon: UsersRound });
|
|
items.push({ label: t("contextMenu.damengRoles"), action: openDamengRoles, icon: ShieldCheck });
|
|
items.push({ label: t("contextMenu.damengJobAdmin"), action: openDamengJobAdmin, icon: CalendarClock });
|
|
}
|
|
if (canCopyFinalProxyPort.value) {
|
|
items.push({ label: t("contextMenu.copyFinalProxyPort"), action: copyFinalProxyPort, icon: Network });
|
|
}
|
|
if (canOpenSqlFileExecution.value) {
|
|
items.push({ label: t("sqlFile.title"), action: openSqlFileExecution, icon: FileCode });
|
|
}
|
|
if (canExportAllDatabases.value) {
|
|
items.push({ label: t("contextMenu.exportAllDatabases"), action: openAllDatabasesExport, icon: Upload });
|
|
}
|
|
if (canOpenScheduledBackups.value) {
|
|
items.push({ label: t("databaseBackup.title"), action: openScheduledBackups, icon: CalendarClock });
|
|
}
|
|
if (canCreateDatabase.value) {
|
|
items.push({
|
|
label: connectionNamespaceCreationLabel(),
|
|
action: openConnectionNamespaceCreation,
|
|
icon: Plus,
|
|
});
|
|
}
|
|
if (canCreateNacosNamespace.value) {
|
|
items.push({
|
|
label: t("nacos.createNamespace"),
|
|
action: openCreateNacosNamespaceDialog,
|
|
icon: FolderPlus,
|
|
});
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
const moveTargets = selectedConnectionMoveTargets(node, selectedTreeNodesInVisibleOrder());
|
|
const allMoveTargetsInGroup = (groupId: string | null) => moveTargets.length > 0 && moveTargets.every((target) => connectionStore.groupIdForConnection(target.connectionId) === groupId);
|
|
if (availableGroups.value.length > 0 || !allMoveTargetsInGroup(null)) {
|
|
const groupChildren: ContextMenuItem[] = availableGroups.value.map((group) => ({
|
|
label: group.name,
|
|
title: group.path.join(" / "),
|
|
action: () => moveToGroup(group.id),
|
|
icon: FolderOpen,
|
|
indentLevel: group.depth,
|
|
disabled: allMoveTargetsInGroup(group.id),
|
|
}));
|
|
if (!allMoveTargetsInGroup(null)) {
|
|
groupChildren.push({ label: "", separator: true });
|
|
groupChildren.push({ label: t("connectionGroup.ungrouped"), action: () => moveToGroup(null) });
|
|
}
|
|
groupChildren.push({ label: "", separator: true });
|
|
groupChildren.push({ label: t("connectionGroup.newGroup"), action: moveToNewGroup, icon: FolderPlus });
|
|
items.push({ label: t("connectionGroup.moveToGroup"), icon: FolderInput, children: groupChildren });
|
|
} else {
|
|
items.push({ label: t("connectionGroup.moveToNewGroup"), action: moveToNewGroup, icon: FolderPlus });
|
|
}
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
const visibleFilterMenu = sidebarConnectionVisibleFilterMenu({
|
|
canConfigureVisibleDatabases: canConfigureVisibleDatabases.value,
|
|
canConfigureVisibleSchemas: canConfigureVisibleSchemas.value,
|
|
databaseFilterUsesSchemas: connectionUsesVisibleSchemaFilter(node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined),
|
|
});
|
|
for (const entry of visibleFilterMenu) {
|
|
items.push({
|
|
label: t(entry.label === "schemas" ? "visibleSchemas.title" : "contextMenu.configureVisibleObjects"),
|
|
action: entry.target === "visible-schemas" ? openVisibleSchemasDialog : openVisibleDatabasesDialog,
|
|
icon: ListFilter,
|
|
});
|
|
}
|
|
if (currentDatabaseType() === "nacos") {
|
|
items.push({
|
|
label: t("nacos.nacosVisibleNamespacesTitle"),
|
|
action: openVisibleNacosNamespacesDialog,
|
|
icon: ListFilter,
|
|
});
|
|
}
|
|
items.push({ label: t("contextMenu.editConnection"), action: editConnection, icon: Pencil, shortcut: shortcutEditConnection.value });
|
|
if (revealConnectionFilePath.value) {
|
|
items.push({
|
|
label: t("contextMenu.revealDatabaseFile"),
|
|
action: revealDatabaseFile,
|
|
icon: FolderOpen,
|
|
});
|
|
}
|
|
if (canBackupSqliteDatabase.value) {
|
|
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
|
const usesSsh = (config?.transport_layers || []).some((layer) => layer.enabled !== false && layer.type === "ssh");
|
|
items.push({
|
|
label: t("contextMenu.backupSqliteDatabase"),
|
|
action: backupSqliteDatabase,
|
|
icon: HardDriveDownload,
|
|
});
|
|
if (usesSsh) {
|
|
items.push({
|
|
label: t("contextMenu.restoreSqliteDatabase"),
|
|
action: restoreSqliteDatabase,
|
|
icon: HardDriveDownload,
|
|
});
|
|
}
|
|
}
|
|
items.push({ label: connectionDuplicateMenuLabel(), action: duplicateConnection, icon: CopyPlus });
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: connectionDeleteMenuLabel(),
|
|
action: deleteConnection,
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
// Plugin-contributed entries must be appended before this branch returns.
|
|
// treeItemMenuItems() stops at the first factory that reports the node as
|
|
// handled, so a call placed after the factory loop never runs for a
|
|
// connection node.
|
|
appendPluginConnectionMenuItems(items, node);
|
|
return true;
|
|
}
|
|
|
|
// 3. Connection Group
|
|
if (node.type === "connection-group") {
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("toolbar.newConnection"), action: newConnectionInGroup, icon: Plus });
|
|
items.push({ label: t("connectionGroup.newGroup"), action: newSubgroup, icon: FolderPlus });
|
|
items.push({
|
|
label: connectionGroupDisconnectMenuLabel(),
|
|
action: disconnectConnectionGroup,
|
|
icon: Unplug,
|
|
disabled: !canDisconnectConnectionGroup(),
|
|
});
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: t("connectionGroup.renameGroup"),
|
|
action: startRenameGroup,
|
|
icon: Pencil,
|
|
shortcut: shortcutRename,
|
|
});
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: connectionGroupDeleteMenuLabel(),
|
|
action: deleteConnectionGroup,
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|
const { node, items } = context;
|
|
appendTableVGroupContainerItems(node, items);
|
|
// 4. Database / Schema
|
|
if (node.type === "database" || node.type === "schema") {
|
|
if (isXuguSyntheticTreeNode(currentDatabaseType(), node.type, node.schema)) {
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
return true;
|
|
}
|
|
if (currentDatabaseType() === "hbase") {
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
if (canCreateTable.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
|
|
}
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
return true;
|
|
}
|
|
if (canCloseDatabaseConnection.value) {
|
|
items.unshift({ label: "", separator: true });
|
|
items.unshift({ label: t("contextMenu.closeDatabaseConnection"), action: closeDatabaseConnection, icon: Unplug });
|
|
}
|
|
items.push(copyNameMenuItem());
|
|
const databaseUrlCopyMenu = connectionUrlCopyMenuItem(node.database || undefined);
|
|
if (databaseUrlCopyMenu) items.push(databaseUrlCopyMenu);
|
|
items.push({ label: "", separator: true });
|
|
if (canOpenObjectBrowser.value) {
|
|
items.push({ label: t("contextMenu.openObjectBrowser"), action: openObjectBrowser, icon: TableProperties });
|
|
}
|
|
if (supportsConnectionQueryActions(currentDatabaseType())) {
|
|
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
|
if (node.type === "database" && supportsAiAssistantContext(currentDatabaseType())) {
|
|
items.push(addToAiMenuItem(node));
|
|
}
|
|
const sqlHistoryMenu = savedSqlHistorySubmenu();
|
|
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
|
|
}
|
|
const profileWorkspace = node.type === "database" && node.connectionId ? driverProfileDatabaseWorkspace(connectionStore.getConfig(node.connectionId)?.driver_profile) : undefined;
|
|
if (profileWorkspace?.entryScopes.includes("database")) {
|
|
items.push({ label: t(profileWorkspace.menuLabelKey), action: openProfileDatabaseWorkspace, icon: GitBranch });
|
|
}
|
|
if (node.type === "database" && currentDatabaseType() !== "cloudflare-d1") {
|
|
if (!isNodeDefaultDatabase.value) {
|
|
items.push({ label: t("contextMenu.setDefaultDatabase"), action: setNodeAsDefaultDatabase, icon: Database });
|
|
} else {
|
|
items.push({ label: t("contextMenu.clearDefaultDatabase"), action: clearNodeDefaultDatabase, icon: Database });
|
|
}
|
|
}
|
|
if (node.type === "schema") {
|
|
if (!isNodeDefaultSchema.value) {
|
|
items.push({ label: t("contextMenu.setDefaultSchema"), action: setNodeAsDefaultSchema, icon: Database });
|
|
} else {
|
|
items.push({ label: t("contextMenu.clearDefaultSchema"), action: clearNodeDefaultSchema, icon: Database });
|
|
}
|
|
}
|
|
if (canEditDatabaseProperties.value) {
|
|
items.push({ label: t("contextMenu.editDatabaseProperties"), action: openEditDatabasePropertiesDialog, icon: SquarePen });
|
|
}
|
|
if (canRenameDatabase.value) {
|
|
items.push({
|
|
label: t("contextMenu.renameDatabase"),
|
|
action: openRenameDatabaseDialog,
|
|
icon: Pencil,
|
|
shortcut: shortcutRename,
|
|
});
|
|
}
|
|
if (canCreateTable.value) {
|
|
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
|
|
}
|
|
if (canOpenTableImport.value) {
|
|
items.push({ label: t("contextMenu.importData"), action: openTableImport, icon: Download });
|
|
}
|
|
if (canCreateSchema.value) {
|
|
items.push({ label: t("contextMenu.createSchema"), action: openCreateSchemaDialog, icon: Plus });
|
|
}
|
|
if (canEditSchemaComment.value) {
|
|
items.push({ label: t("contextMenu.editSchemaComment"), action: openEditSchemaCommentDialog, icon: SquarePen });
|
|
}
|
|
if (canOpenSqlFileExecution.value) {
|
|
items.push({ label: t("sqlFile.title"), action: openSqlFileExecution, icon: FileCode });
|
|
}
|
|
if (canOpenDiagram.value) {
|
|
items.push({ label: t("diagram.open"), action: openDiagram, icon: Network });
|
|
items.push({ label: t("docs.title"), action: openDocs, icon: BookOpen });
|
|
}
|
|
if (canOpenDatabaseSearch.value) {
|
|
items.push({ label: t("databaseSearch.open"), action: openDatabaseSearch, icon: Search });
|
|
}
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
if (canConfigureVisibleSchemas.value) {
|
|
items.push({
|
|
label: t("visibleSchemas.title"),
|
|
action: openVisibleSchemasDialog,
|
|
icon: ListFilter,
|
|
});
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("transfer.dataTransfer"), action: openTransfer, icon: ArrowRightLeft });
|
|
items.push({ label: t("diff.title"), action: openSchemaDiff, icon: ArrowRightLeft });
|
|
items.push({ label: t("dataCompare.title"), action: openDataCompare, icon: ArrowRightLeft });
|
|
items.push({ label: t("contextMenu.exportDatabase"), action: openDatabaseExport, icon: Upload });
|
|
const destructiveActions: ContextMenuItem[] = [];
|
|
if (canDropDatabase.value) {
|
|
destructiveActions.push({
|
|
label: t("contextMenu.dropDatabase"),
|
|
action: dropDatabase,
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
if (destructiveActions.length > 0) {
|
|
items.push({ label: "", separator: true });
|
|
items.push(moreActionsSubmenu(destructiveActions));
|
|
}
|
|
if (canDropSchema.value) {
|
|
items.push({ label: "", separator: true });
|
|
}
|
|
if (canDropSchema.value) {
|
|
items.push({
|
|
label: t("contextMenu.dropSchema"),
|
|
action: dropSchema,
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|
const { node, items } = context;
|
|
|
|
if (node.type === "saved-sql-root") {
|
|
items.push({
|
|
label: t("savedSql.pasteFile"),
|
|
action: () => requestPasteTreeClipboard(),
|
|
icon: Clipboard,
|
|
shortcut: shortcutPaste.value,
|
|
disabled: connectionStore.treeClipboard?.kind !== "saved-sql-copy" || !savedSqlPasteTargetForNode(node),
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "saved-sql-file") {
|
|
items.push({ label: t("savedSql.open"), action: openSavedSqlFile, icon: FileCode });
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("savedSql.copyFile"), action: copySavedSqlFiles, icon: Copy, shortcut: shortcutCopyName.value });
|
|
items.push({
|
|
label: t("savedSql.pasteFile"),
|
|
action: () => requestPasteTreeClipboard(),
|
|
icon: Clipboard,
|
|
shortcut: shortcutPaste.value,
|
|
disabled: connectionStore.treeClipboard?.kind !== "saved-sql-copy" || !savedSqlPasteTargetForNode(node),
|
|
});
|
|
items.push({ label: t("sqlLibrary.exportFile"), action: exportSavedSqlFile, icon: Upload });
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("savedSql.renameFile"), action: renameSavedSqlFile, icon: Pencil, shortcut: shortcutRename });
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("savedSql.deleteFile"), action: deleteSavedSqlFile, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
|
|
return true;
|
|
}
|
|
// 5. Redis DB / Mongo DB
|
|
if (currentDatabaseType() === "hbase" && node.type === "group-tables") {
|
|
if (canCreateTable.value) {
|
|
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
|
|
items.push({ label: "", separator: true });
|
|
}
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (currentDatabaseType() === "hbase" && node.type === "table") {
|
|
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: openDataImmediately, icon: TableProperties });
|
|
items.push({
|
|
label: t("contextMenu.openInNewDataTab"),
|
|
action: openDataInNewTabImmediately,
|
|
icon: CopyPlus,
|
|
shortcut: shortcutOpenDataInNewTab.value,
|
|
});
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
if (!connectionIsEffectivelyReadOnly(connectionStore.getConfig(node.connectionId || ""))) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: t("hbase.deleteTable"),
|
|
action: requestDeleteHBaseTable,
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "nacos-access-control" || node.type === "etcd-root" || node.type === "etcd-dashboard" || node.type === "etcd-access-control" || node.type === "zookeeper-root" || node.type === "consul-root" || node.type === "consul-overview") {
|
|
items.push({ label: t("contextMenu.openConnection"), action: toggle, icon: Database });
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "user-admin") {
|
|
items.push({ label: t("contextMenu.openUserAdmin"), action: openUserAdmin, icon: UsersRound });
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "dameng-job-admin") {
|
|
items.push({ label: t("contextMenu.openDamengJobAdmin"), action: openDamengJobAdmin, icon: CalendarClock });
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "redis-db" || node.type === "mongo-db" || node.type === "vector-database") {
|
|
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
|
if (node.type === "mongo-db" && canOpenObjectBrowser.value) {
|
|
items.push({ label: t("contextMenu.openObjectBrowser"), action: openObjectBrowser, icon: TableProperties });
|
|
}
|
|
if (!isNodeDefaultDatabase.value) {
|
|
items.push({ label: t("contextMenu.setDefaultDatabase"), action: setNodeAsDefaultDatabase, icon: Database });
|
|
} else {
|
|
items.push({ label: t("contextMenu.clearDefaultDatabase"), action: clearNodeDefaultDatabase, icon: Database });
|
|
}
|
|
if (node.type === "mongo-db") {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("transfer.dataTransfer"), action: openTransfer, icon: ArrowRightLeft });
|
|
items.push({ label: t("mongoDump.menuDump"), action: () => openMongoDatabaseDump("dump"), icon: Upload });
|
|
items.push({ label: t("mongoDump.menuRestore"), action: () => openMongoDatabaseDump("restore"), icon: Download });
|
|
}
|
|
if (node.type === "redis-db") {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("redis.setDatabaseAlias"), action: openRedisDatabaseAliasDialog, icon: Pencil });
|
|
items.push({ label: t("redis.flushDb"), action: flushRedisDb, icon: Eraser, variant: "destructive" as const });
|
|
}
|
|
if (canDropMongoDatabase.value || canDropMilvusDatabase.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: t("contextMenu.dropDatabase"),
|
|
action: dropDatabase,
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "nacos-namespace") {
|
|
items.push({ label: t("contextMenu.openConnection"), action: toggle, icon: FolderOpen });
|
|
if (canEditNacosNamespace.value) {
|
|
items.push({ label: t("nacos.editNamespace"), action: openEditNacosNamespaceDialog, icon: Pencil });
|
|
}
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
if (canDeleteNacosNamespace.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: t("nacos.deleteNamespace"),
|
|
action: openDeleteNacosNamespaceConfirm,
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "dynamodb-table") {
|
|
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 });
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "mongo-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 });
|
|
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
|
// Creating and dropping indexes stay on the Indexes group node; the collection
|
|
// only opens the manager panel, which offers creation from inside itself.
|
|
if (canManageMongoIndexes.value) {
|
|
items.push({ label: t("contextMenu.manageMongoIndexes"), action: openMongoIndexManagerDialog, icon: PencilRuler });
|
|
}
|
|
if (canRenameMongoCollection.value) {
|
|
items.push({
|
|
label: t("contextMenu.renameObject"),
|
|
action: openRenameMongoCollectionDialog,
|
|
icon: Pencil,
|
|
shortcut: shortcutRename,
|
|
});
|
|
}
|
|
if (canCloneMongoCollection.value) {
|
|
items.push({ label: t("contextMenu.cloneCollection"), action: openCloneMongoCollectionDialog, icon: CopyPlus });
|
|
}
|
|
items.push({ label: t("contextMenu.importData"), action: openMongoImport, icon: Download });
|
|
items.push({
|
|
label: t("contextMenu.exportData"),
|
|
icon: Upload,
|
|
children: [
|
|
{ label: "CSV", action: () => void exportMongoCollection("csv") },
|
|
{ label: "NDJSON", action: () => void exportMongoCollection("ndjson") },
|
|
{ label: "BSON dump", action: () => void exportMongoCollection("bson") },
|
|
{ label: "BSON dump (gzip)", action: () => void exportMongoCollection("bsonGzip") },
|
|
],
|
|
});
|
|
if (canDropMongoCollection.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.dropCollection"), action: dropMongoCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "elasticsearch-index" || node.type === "vector-collection") {
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
// Meilisearch indexes open through their dedicated search workspace; the
|
|
// generic data/query actions are not valid for this connection type.
|
|
const isMeilisearchIndex = currentDatabaseType() === "meilisearch";
|
|
const hasAdditionalIndexActions = canRenameMongoCollection.value || canManageElasticsearchIndex.value || canDropMilvusCollection.value;
|
|
if (!isMeilisearchIndex || hasAdditionalIndexActions) {
|
|
items.push({ label: "", separator: true });
|
|
}
|
|
if (!isMeilisearchIndex) {
|
|
items.push({ label: t("contextMenu.viewData"), action: toggle, icon: TableProperties });
|
|
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
|
}
|
|
if (canRenameMongoCollection.value) {
|
|
items.push({ label: t("contextMenu.renameObject"), action: openRenameMongoCollectionDialog, icon: Pencil, shortcut: shortcutRename });
|
|
}
|
|
if (canManageElasticsearchIndex.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.elasticsearchViewMapping"), action: () => openElasticsearchIndexMetadata("mapping"), icon: Braces });
|
|
items.push({ label: t("contextMenu.elasticsearchViewSettings"), action: () => openElasticsearchIndexMetadata("settings"), icon: Settings2 });
|
|
items.push({ label: t("contextMenu.elasticsearchViewStats"), action: () => openElasticsearchIndexMetadata("stats"), icon: BarChart3 });
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.elasticsearchClearIndex"), action: clearElasticsearchIndex, icon: Eraser, variant: "destructive" as const });
|
|
}
|
|
if (canDropMilvusCollection.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.dropCollection"), action: dropMilvusCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 8.5 Extension
|
|
if (node.type === "extension") {
|
|
items.push({ label: t("extension.viewDetails"), action: () => emit("open-extension-details", node), icon: Info });
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|
const { node, items, deleteMenuLabel, deleteMenuAction, truncateMenuLabel, truncateMenuAction, emptyMenuLabel, emptyMenuAction, batchAutoIncrementCount, autoIncrementMenuLabel, autoIncrementMenuAction } = context;
|
|
// 6. Table / View / Materialized View
|
|
if (node.type === "table" || node.type === "view" || node.type === "materialized_view") {
|
|
if (currentDatabaseType() === "victoriametrics" && node.type === "table") {
|
|
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: openDataImmediately, icon: TableProperties });
|
|
items.push({ label: t("contextMenu.openInNewDataTab"), action: openDataInNewTabImmediately, icon: CopyPlus });
|
|
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
|
if (supportsAiAssistantContext(currentDatabaseType())) {
|
|
items.push(addToAiMenuItem(node));
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push(exportDataSubmenu(false));
|
|
items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw, shortcut: shortcutRefresh });
|
|
return true;
|
|
}
|
|
const destructiveActions: ContextMenuItem[] = [];
|
|
items.push(copyNameMenuItem());
|
|
if (node.type === "table") {
|
|
const vgroupMoveItems = buildTableVGroupMoveMenuItems(node);
|
|
if (vgroupMoveItems.length) items.push({ label: t("tableVGroup.moveToGroup"), icon: FolderInput, children: vgroupMoveItems });
|
|
}
|
|
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
|
if (node.type === "table" && supportsAiAssistantContext(currentDatabaseType())) {
|
|
items.push(addToAiMenuItem(node));
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.viewData"), action: openDataImmediately, icon: TableProperties });
|
|
items.push({
|
|
label: t("contextMenu.openInNewDataTab"),
|
|
action: openDataInNewTabImmediately,
|
|
icon: CopyPlus,
|
|
shortcut: shortcutOpenDataInNewTab.value,
|
|
});
|
|
if (node.type === "table") {
|
|
items.push({
|
|
label: t("contextMenu.viewDdl"),
|
|
action: openDdl,
|
|
icon: FileCode,
|
|
});
|
|
}
|
|
if (canViewDatabaseObjectDependencies(currentDatabaseType(), node)) {
|
|
items.push({ label: t("contextMenu.viewDependencies"), action: openDatabaseObjectDependencies, icon: Network });
|
|
}
|
|
if (node.type === "view" || node.type === "materialized_view") {
|
|
items.push({ label: t("contextMenu.editView"), action: () => openObjectSourceDialog(true), icon: Pencil });
|
|
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
|
if (node.type === "view" && currentDatabaseType() === "dameng" && buildDamengCompileViewSql({ schema: node.schema, name: node.objectName || node.label })) {
|
|
items.push({ label: t("contextMenu.compileObject"), action: compileDamengView, icon: Wrench });
|
|
}
|
|
items.push({
|
|
label: t("contextMenu.viewDdl"),
|
|
action: openDdl,
|
|
icon: FileCode,
|
|
});
|
|
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
|
}
|
|
if (canOpenStructureEditor.value) {
|
|
items.push({ label: t("contextMenu.editStructure"), action: openStructureEditor, icon: PencilRuler });
|
|
}
|
|
if (canRenameObject.value) {
|
|
items.push({
|
|
label: t("contextMenu.renameObject"),
|
|
action: openRenameObjectDialog,
|
|
icon: Pencil,
|
|
shortcut: shortcutRename,
|
|
});
|
|
}
|
|
if (node.type === "view" || node.type === "materialized_view") {
|
|
destructiveActions.push({
|
|
label: deleteMenuLabel(t("contextMenu.dropView")),
|
|
action: deleteMenuAction(requestDropObject),
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
items.push({
|
|
label: t("contextMenu.generateSql"),
|
|
icon: FilePlus,
|
|
children: isTableNotView.value
|
|
? [
|
|
{ label: "SELECT", action: newSelectTemplate, icon: TerminalSquare },
|
|
{ label: "INSERT", action: newInsertTemplate, icon: FilePlus },
|
|
{ label: "UPDATE", action: newUpdateTemplate, icon: SquarePen },
|
|
{ label: "DELETE", action: newDeleteTemplate, icon: ListX },
|
|
{ label: "DDL", action: generateDdlTemplate, icon: FileCode },
|
|
]
|
|
: [
|
|
{ label: "SELECT", action: newSelectTemplate, icon: TerminalSquare },
|
|
{ label: "DDL", action: generateDdlTemplate, icon: FileCode },
|
|
],
|
|
});
|
|
const sqlHistoryMenu = savedSqlHistorySubmenu();
|
|
if (sqlHistoryMenu) items.push(sqlHistoryMenu);
|
|
if (canOpenDiagram.value) {
|
|
items.push({ label: t("diagram.open"), action: openDiagram, icon: Network });
|
|
}
|
|
if (canOpenTableImport.value) {
|
|
items.push({ label: t("contextMenu.importData"), action: openTableImport, icon: Download });
|
|
}
|
|
if (isTableNotView.value) {
|
|
items.push({ label: t("dataCompare.title"), action: openDataCompare, icon: ArrowRightLeft });
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push(exportDataSubmenu());
|
|
items.push({ label: t("contextMenu.exportDatabase"), action: openDatabaseExport, icon: Upload });
|
|
items.push({ label: t("contextMenu.exportStructure"), action: exportStructure, icon: FileCode });
|
|
items.push(copyStructureAsSubmenu());
|
|
if (isTableNotView.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.duplicateStructure"), action: duplicateStructure, icon: CopyPlus });
|
|
// Keep menu copy aligned with keyboard copy so frozen multi-selection and single-row fallback stay compatible.
|
|
items.push(...treeTableClipboardMenuItems(node));
|
|
if (supportsVacuum.value) {
|
|
destructiveActions.push({ label: t("contextMenu.vacuumTable"), action: vacuumTable, icon: Activity, variant: "destructive" as const });
|
|
}
|
|
if (supportsMysqlAutoIncrement.value || batchAutoIncrementCount > 1) {
|
|
items.push({ label: autoIncrementMenuLabel(t("contextMenu.mysqlAutoIncrement")), action: autoIncrementMenuAction(mysqlAutoIncrement), icon: Gauge });
|
|
}
|
|
if (supportsTruncate.value) {
|
|
destructiveActions.push({
|
|
label: truncateMenuLabel(t("contextMenu.truncateTable")),
|
|
action: truncateMenuAction(truncateTable),
|
|
icon: Scissors,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
destructiveActions.push({
|
|
label: emptyMenuLabel(t("contextMenu.emptyTable")),
|
|
action: emptyMenuAction(emptyTable),
|
|
icon: Eraser,
|
|
variant: "destructive" as const,
|
|
});
|
|
destructiveActions.push({
|
|
label: deleteMenuLabel(t("contextMenu.dropTable")),
|
|
action: deleteMenuAction(dropTable),
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
if (destructiveActions.length > 0) {
|
|
items.push({ label: "", separator: true });
|
|
items.push(moreActionsSubmenu(destructiveActions));
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// 7. Column
|
|
if (node.type === "column") {
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
const columnActions: ContextMenuItem[] = [];
|
|
if (canOpenStructureEditor.value) {
|
|
columnActions.push({ label: t("contextMenu.editColumn"), action: openStructureEditor, icon: PencilRuler });
|
|
}
|
|
if (canOpenFieldLineage.value) {
|
|
columnActions.push({ label: t("lineage.open"), action: openFieldLineage, icon: Network });
|
|
}
|
|
if (columnActions.length > 0) {
|
|
items.push({ label: "", separator: true });
|
|
items.push(...columnActions);
|
|
}
|
|
if (canDropTableChildObject.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: deleteMenuLabel(dropTableChildObjectMenuLabel()),
|
|
action: deleteMenuAction(requestDropTableChildObject),
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "index" || node.type === "fkey" || (node.type === "trigger" && !!node.tableName)) {
|
|
items.push(node.type === "trigger" ? copyNameMenuItem() : { label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
if (currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
|
|
items.push({ label: t("contextMenu.compileObject"), action: compileXuguObject, icon: Wrench });
|
|
}
|
|
if (canViewDatabaseObjectDependencies(currentDatabaseType(), node)) {
|
|
items.push({ label: t("contextMenu.viewDependencies"), action: openDatabaseObjectDependencies, icon: Network });
|
|
}
|
|
if (node.type === "index" && canOpenStructureEditor.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.editIndex"), action: openStructureEditor, icon: PencilRuler });
|
|
}
|
|
if (node.type === "index" && canDropMongoIndex.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: deleteMenuLabel(t("contextMenu.dropIndex")),
|
|
action: deleteMenuAction(dropMongoIndex),
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
} else if (canDropTableChildObject.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: deleteMenuLabel(dropTableChildObjectMenuLabel()),
|
|
action: deleteMenuAction(requestDropTableChildObject),
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 8. Procedure / Function / Package
|
|
if (node.type === "procedure" || node.type === "function") {
|
|
const isPackageMember = node.parentType === "package" && !!node.parentName;
|
|
if (node.type === "procedure" && !isPackageMember) {
|
|
items.push({ label: t("contextMenu.executeProcedure"), action: openProcedureExecution, icon: Play });
|
|
}
|
|
if (!isPackageMember && currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
|
|
items.push({ label: t("contextMenu.compileObject"), action: compileXuguObject, icon: Wrench });
|
|
}
|
|
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
|
if (!isPackageMember) {
|
|
items.push({ label: t("diff.title"), action: openSchemaDiffForRoutine, icon: ArrowRightLeft });
|
|
}
|
|
if (!isPackageMember && canViewDatabaseObjectDependencies(currentDatabaseType(), node)) {
|
|
items.push({ label: t("contextMenu.viewDependencies"), action: openDatabaseObjectDependencies, icon: Network });
|
|
}
|
|
if (currentDatabaseType() === "mysql") {
|
|
items.push(copyNameMenuItem());
|
|
}
|
|
if (!isPackageMember && canRenameObject.value) {
|
|
items.push({
|
|
label: t("contextMenu.renameObject"),
|
|
action: openRenameObjectDialog,
|
|
icon: Pencil,
|
|
shortcut: shortcutRename,
|
|
});
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
|
if (!isPackageMember) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: deleteMenuLabel(node.type === "procedure" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction")),
|
|
action: deleteMenuAction(requestDropObject),
|
|
icon: Trash2,
|
|
shortcut: shortcutDelete,
|
|
variant: "destructive" as const,
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "event") {
|
|
// Event definitions use the dedicated editor hosted by ObjectBrowser.
|
|
// Open that surface from the tree instead of routing through the generic source dialog.
|
|
items.push({ label: t("contextMenu.editObject"), action: () => openObjectBrowser(false, true), icon: Pencil });
|
|
items.push({ label: t("contextMenu.viewObject"), action: () => openObjectBrowser(true, true), icon: Eye });
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
items.push({ label: t("contextMenu.dropObject"), action: deleteMenuAction(requestDropObject), icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
|
|
items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw, shortcut: shortcutRefresh });
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "sequence" || (node.type === "synonym" && currentDatabaseType() === "oceanbase-oracle")) {
|
|
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
|
if (currentDatabaseType() === "oceanbase-oracle") {
|
|
items.push({ label: t("contextMenu.editObject"), action: () => openObjectSourceDialog(true), icon: Pencil });
|
|
}
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "job") {
|
|
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
if (currentDatabaseType() === "xugu") {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("xuguSchedulerJob.enable"), action: () => executeXuguSchedulerJobAction("enable"), icon: Play });
|
|
items.push({ label: t("xuguSchedulerJob.disable"), action: () => executeXuguSchedulerJobAction("disable"), icon: Wrench });
|
|
items.push({ label: t("xuguSchedulerJob.run"), action: () => executeXuguSchedulerJobAction("run"), icon: Play });
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("xuguSchedulerJob.drop"), action: () => executeXuguSchedulerJobAction("drop"), icon: Trash2, variant: "destructive" as const });
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw, shortcut: shortcutRefresh });
|
|
return true;
|
|
}
|
|
|
|
if (node.type === "trigger" || node.type === "package" || node.type === "package-body") {
|
|
if (currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
|
|
items.push({ label: t("contextMenu.compileObject"), action: compileXuguObject, icon: Wrench });
|
|
}
|
|
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
|
if (canViewDatabaseObjectDependencies(currentDatabaseType(), node)) {
|
|
items.push({ label: t("contextMenu.viewDependencies"), action: openDatabaseObjectDependencies, icon: Network });
|
|
}
|
|
if (node.type === "package" && currentDatabaseType() === "xugu" && node.xuguPackageBodyAvailable === true) {
|
|
items.push({
|
|
label: `${t("contextMenu.viewSource")} (${t("objects.packageBody")})`,
|
|
action: () => openObjectSourceDialog(false, true),
|
|
icon: Code2,
|
|
});
|
|
}
|
|
items.push(node.type === "trigger" ? copyNameMenuItem() : { label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
|
return true;
|
|
}
|
|
|
|
// PostgreSQL-family types expand in the tree and expose DDL as a lightweight
|
|
// copy action. Xugu keeps its source/change-open-mode entries.
|
|
if (node.type === "type" || node.type === "type-body") {
|
|
if (supportsTypeObjectSource(currentDatabaseType())) {
|
|
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
|
return true;
|
|
}
|
|
if (customTypeCapabilities(currentDatabaseType()).details) {
|
|
items.push({ label: t("contextMenu.copyDdl"), action: copyCustomTypeDdl, icon: Copy });
|
|
items.push({ label: "", separator: true });
|
|
}
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function treeTableClipboardMenuItems(node: TreeNode): ContextMenuItem[] {
|
|
if (currentDatabaseType() === "victoriametrics") return [];
|
|
const copyItem: ContextMenuItem = { label: t("contextMenu.copyTable"), action: copySelectedNames, icon: Copy };
|
|
if (!node.connectionId || !node.database) return [copyItem];
|
|
const state = tableClipboardMenuState(
|
|
normalizedTreeClipboardTableEntries(),
|
|
{
|
|
connectionId: node.connectionId,
|
|
database: node.database,
|
|
schema: normalizeTreeClipboardSchema(node.connectionId, node.database, node.schema),
|
|
tableName: node.label,
|
|
},
|
|
canTransferTreeClipboardToCurrentNode(),
|
|
);
|
|
if (state === "copy") return [copyItem];
|
|
const pasteItem: ContextMenuItem = { label: t("contextMenu.pasteTable"), action: openPasteTableDialog, icon: Clipboard };
|
|
return state === "paste" ? [pasteItem] : [copyItem, pasteItem];
|
|
}
|
|
|
|
function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|
const { node, items } = context;
|
|
appendTableVGroupContainerItems(node, items);
|
|
// 9. Group Labels (group-columns, group-tables, etc.)
|
|
if (isGroupLabel(node)) {
|
|
const mysqlObjectTemplate = node.connectionId ? mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node) : null;
|
|
const hasMongoCreateIndexAction = node.type === "group-indexes" && canCreateMongoIndex.value;
|
|
const hasMongoDropAllIndexesAction = node.type === "group-indexes" && canDropAllMongoIndexes.value;
|
|
const hasGroupAction = (node.type === "group-tables" && canCreateTable.value) || (node.type === "group-views" && !!node.connectionId && !!node.database) || !!mysqlObjectTemplate || hasMongoCreateIndexAction || hasMongoDropAllIndexesAction;
|
|
const canLoadAllObjectGroup = node.type === "group-tables" || node.type === "group-dolt-system-tables" || node.type === "group-views" || node.type === "group-materialized-views";
|
|
if (node.type === "group-tables" && canCreateTable.value) {
|
|
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
|
|
if (canOpenTableImport.value) {
|
|
items.push({ label: t("contextMenu.importData"), action: openTableImport, icon: Upload });
|
|
}
|
|
if (canPasteTreeClipboardToCurrentNode() || canTransferTreeClipboardToCurrentNode()) {
|
|
items.push({ label: t("contextMenu.pasteTable"), action: openPasteTableDialog, icon: Clipboard });
|
|
}
|
|
}
|
|
if (node.type === "group-views" && node.connectionId && node.database) {
|
|
items.push({ label: t("contextMenu.createView"), action: createView, icon: Plus });
|
|
}
|
|
if (node.type === "group-events" && node.connectionId && node.database) {
|
|
items.push({ label: t("contextMenu.createEvent"), action: openMysqlEventCreateEditor, icon: Plus });
|
|
}
|
|
if (mysqlObjectTemplate) {
|
|
items.push({ label: t(mysqlObjectTemplate.titleKey), action: createMysqlObjectTemplate, icon: Plus });
|
|
}
|
|
if (hasMongoCreateIndexAction) {
|
|
items.push({ label: t("contextMenu.createMongoIndex"), action: openCreateMongoIndexDialog, icon: Plus });
|
|
}
|
|
if (hasMongoDropAllIndexesAction) {
|
|
if (hasMongoCreateIndexAction) items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.dropAllIndexes"), action: dropAllMongoIndexes, icon: Trash2, variant: "destructive" as const });
|
|
}
|
|
if (hasGroupAction) {
|
|
items.push({ label: "", separator: true });
|
|
}
|
|
if (node.type === "group-extensions") {
|
|
items.push({
|
|
label: t("contextMenu.manageExtension"),
|
|
action: () => openInstallExtensionDialog(node),
|
|
icon: Plus,
|
|
});
|
|
items.push({ label: "", separator: true });
|
|
}
|
|
if (canLoadAllObjectGroup) {
|
|
items.push({
|
|
label: t("contextMenu.expandAll"),
|
|
action: loadAllObjectGroupChildren,
|
|
icon: ChevronsDown,
|
|
disabled: node.isLoading,
|
|
});
|
|
}
|
|
if (supportsSidebarObjectNameFilter(node)) {
|
|
items.push({
|
|
label: t("contextMenu.tableNameFilters"),
|
|
action: () => emit("open-table-name-filters", node),
|
|
icon: ListFilter,
|
|
});
|
|
}
|
|
if (node.type !== "group-partitions") {
|
|
items.push({
|
|
label: t("contextMenu.refreshChildren"),
|
|
action: refresh,
|
|
icon: RefreshCw,
|
|
shortcut: shortcutRefresh,
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function buildTableVGroupMoveMenuItems(node: TreeNode): ContextMenuItem[] {
|
|
const layout = connectionStore.tableVGroupLayoutFor(node);
|
|
const targets = selectedTableVGroupMoveTargets(node, selectedTreeNodesInVisibleOrder());
|
|
const targetNames = targets.map((target) => target.label);
|
|
const targetsInGroup = (groupId: string) => targetNames.every((name) => tableVGroupPathForTable(layout, name).includes(groupId));
|
|
const items: ContextMenuItem[] = tableVGroupDestinationRows(layout).map((row) => ({
|
|
label: row.name,
|
|
title: row.path.join(" / "),
|
|
disabled: targetNames.length > 0 && targetsInGroup(row.id),
|
|
action: () => {
|
|
for (const name of targetNames) connectionStore.moveTableToVGroup(node, name, row.id);
|
|
},
|
|
}));
|
|
if (targetNames.some((name) => tableVGroupPathForTable(layout, name).length > 0)) {
|
|
items.push({
|
|
label: t("tableVGroup.removeFromGroup"),
|
|
action: () => {
|
|
for (const name of targetNames) connectionStore.moveTableToVGroup(node, name, null);
|
|
},
|
|
});
|
|
}
|
|
items.push({ label: "", separator: true });
|
|
items.push({
|
|
label: t("tableVGroup.moveToNewGroup"),
|
|
action: () => {
|
|
tableVGroupDialogScope.value = node;
|
|
tableVGroupDialogParentGroupId.value = null;
|
|
tableVGroupDialogTableNames.value = targetNames;
|
|
tableVGroupName.value = "";
|
|
showTableVGroupDialog.value = true;
|
|
},
|
|
icon: FolderPlus,
|
|
});
|
|
return items;
|
|
}
|
|
|
|
function appendTableVGroupContainerItems(node: TreeNode, items: ContextMenuItem[]) {
|
|
if (node.type !== "database" && node.type !== "schema" && node.type !== "linked-server-schema" && node.type !== "group-tables") return;
|
|
const layout = connectionStore.tableVGroupLayoutFor(node);
|
|
if (!hasTableVGroupEntries(layout)) return;
|
|
const enabled = tableVGroupsEnabled(layout);
|
|
items.push({
|
|
label: enabled ? t("tableVGroup.disableGroups") : t("tableVGroup.enableGroups"),
|
|
action: () => connectionStore.setTableVGroupsEnabled(node, !enabled),
|
|
icon: FolderInput,
|
|
});
|
|
items.push({ label: "", separator: true });
|
|
}
|
|
|
|
function buildTableVGroupSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|
const { node, items } = context;
|
|
if (node.type !== "table-vgroup" || !node.vgroupId) return false;
|
|
const groupId = node.vgroupId;
|
|
items.push({
|
|
label: t("tableVGroup.newSubgroup"),
|
|
action: () => {
|
|
tableVGroupDialogScope.value = node;
|
|
tableVGroupDialogParentGroupId.value = groupId;
|
|
tableVGroupDialogTableNames.value = [];
|
|
tableVGroupName.value = "";
|
|
showTableVGroupDialog.value = true;
|
|
},
|
|
icon: FolderPlus,
|
|
});
|
|
items.push({ label: t("connectionGroup.renameGroup"), action: () => emit("request-group-rename", node.id), icon: Pencil, shortcut: shortcutRename });
|
|
items.push({
|
|
label: t("tableVGroup.deleteGroup"),
|
|
action: () => requestTableVGroupDelete(node),
|
|
icon: Trash2,
|
|
variant: "destructive" as const,
|
|
shortcut: shortcutDelete,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
/** Open the shared confirmation before removing a group (its tables stay). */
|
|
function requestTableVGroupDelete(node: TreeNode) {
|
|
if (node.type !== "table-vgroup" || !node.vgroupId) return;
|
|
tableVGroupDeleteTarget.value = { scope: node, groupId: node.vgroupId, name: node.label };
|
|
showTableVGroupDeleteConfirm.value = true;
|
|
}
|
|
|
|
const sidebarMenuFactories: readonly SidebarMenuFactory[] = [buildConnectionSidebarMenu, buildDatabaseSidebarMenu, buildSpecialSidebarMenu, buildTableVGroupSidebarMenu, buildObjectSidebarMenu, buildObjectGroupSidebarMenu];
|
|
|
|
function treeItemMenuItems(): ContextMenuItem[] {
|
|
const node = activeNode.value;
|
|
const items: ContextMenuItem[] = [];
|
|
const batchDropCount = selectedBatchDropTargets().length;
|
|
const batchEmptyCount = selectedBatchEmptyTargets().length;
|
|
const batchTruncateCount = selectedBatchTruncateTargets().length;
|
|
const batchAutoIncrementCount = selectedBatchMysqlAutoIncrementTargets().length;
|
|
const deleteMenuLabel = (singleLabel: string) => (batchDropCount > 1 ? batchDropMenuLabel() : singleLabel);
|
|
const deleteMenuAction = (singleAction: () => void) => (batchDropCount > 1 ? requestBatchDrop : singleAction);
|
|
const truncateMenuLabel = (singleLabel: string) => (batchTruncateCount > 1 ? batchTruncateMenuLabel() : singleLabel);
|
|
const truncateMenuAction = (singleAction: () => void) => (batchTruncateCount > 1 ? requestBatchTruncate : singleAction);
|
|
const emptyMenuLabel = (singleLabel: string) => (batchEmptyCount > 1 ? batchEmptyMenuLabel() : singleLabel);
|
|
const emptyMenuAction = (singleAction: () => void) => (batchEmptyCount > 1 ? requestBatchEmpty : singleAction);
|
|
const autoIncrementMenuLabel = (singleLabel: string) => (batchAutoIncrementCount > 1 ? batchMysqlAutoIncrementMenuLabel() : singleLabel);
|
|
const autoIncrementMenuAction = (singleAction: () => void) => (batchAutoIncrementCount > 1 ? requestBatchMysqlAutoIncrement : singleAction);
|
|
|
|
// 1. Pin toggle
|
|
if (canPin.value) {
|
|
items.push({
|
|
label: isPinned.value ? t("contextMenu.unpin") : t("contextMenu.pin"),
|
|
action: togglePin,
|
|
icon: Pin,
|
|
});
|
|
if (hasTypeMenu.value) items.push({ label: "", separator: true });
|
|
}
|
|
const factoryContext: SidebarMenuFactoryContext = {
|
|
node,
|
|
items,
|
|
deleteMenuLabel,
|
|
deleteMenuAction,
|
|
truncateMenuLabel,
|
|
truncateMenuAction,
|
|
emptyMenuLabel,
|
|
emptyMenuAction,
|
|
batchAutoIncrementCount,
|
|
autoIncrementMenuLabel,
|
|
autoIncrementMenuAction,
|
|
};
|
|
for (const factory of sidebarMenuFactories) {
|
|
if (factory(factoryContext)) return items;
|
|
}
|
|
|
|
// 10. Universal Copy Name (for all types except connection)
|
|
if (hasTypeMenu.value) {
|
|
items.push({ label: "", separator: true });
|
|
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
/** Plugin-contributed native menu entries for saved connections. */
|
|
function appendPluginConnectionMenuItems(items: ContextMenuItem[], node: TreeNode) {
|
|
if (node.type !== "connection") return;
|
|
const pluginItems = sidebarPluginRegistry.value.listContextMenuItems("connection");
|
|
if (pluginItems.length === 0) return;
|
|
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
|
if (!config) return;
|
|
items.push({ label: "", separator: true });
|
|
for (const { plugin, contribution } of pluginItems) {
|
|
items.push({
|
|
label: contribution.label,
|
|
icon: PlugZap,
|
|
action: () => {
|
|
api
|
|
.invokePlugin(plugin.manifest.id, `contextMenu/${contribution.id}`, {
|
|
connection: { id: config.id, dbType: config.db_type, name: config.name, database: config.database || "" },
|
|
})
|
|
.then((result) => {
|
|
const message = (result as { message?: unknown } | null | undefined)?.message;
|
|
if (typeof message === "string" && message.trim()) toast(message, 4000);
|
|
})
|
|
.catch((error: unknown) => {
|
|
toast(String((error as Error)?.message || error), 5000);
|
|
});
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function activateRuntimeNode(node: TreeNode) {
|
|
activeNode.value = node;
|
|
}
|
|
|
|
// Async loaders can rebuild a connection node while awaiting the backend. Keep
|
|
// the live node active for later actions, but publish the rendered node so the
|
|
// tree owner can synchronize display projections without losing the toggle.
|
|
function emitNodeToggled(node: TreeNode, wasExpanded: boolean, expandedOverride?: boolean) {
|
|
const liveNode = findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? node;
|
|
activeNode.value = liveNode;
|
|
emit("node-toggled", node, expandedOverride ?? !wasExpanded);
|
|
}
|
|
|
|
function activateActionTarget(target: SidebarActionTarget) {
|
|
activeNode.value = findSidebarActionTarget(connectionStore.treeNodes, target) ?? target;
|
|
}
|
|
|
|
let acceptedSelectionOwner: symbol | null = null;
|
|
|
|
function bindMenuTarget(items: ContextMenuItem[], target: SidebarActionTarget, selectionIds: readonly string[]): ContextMenuItem[] {
|
|
return items.map((item) => ({
|
|
...item,
|
|
action: item.action
|
|
? () => {
|
|
const owner = Symbol("sidebar-menu-action");
|
|
acceptedSelectionOwner = owner;
|
|
acceptedSelectionIds = selectionIds;
|
|
activateActionTarget(target);
|
|
const result = item.action?.() as unknown;
|
|
if (result && typeof (result as PromiseLike<unknown>).then === "function") {
|
|
return Promise.resolve(result).finally(() => {
|
|
if (acceptedSelectionOwner !== owner) return;
|
|
acceptedSelectionOwner = null;
|
|
acceptedSelectionIds = null;
|
|
});
|
|
}
|
|
if (acceptedSelectionOwner === owner) {
|
|
acceptedSelectionOwner = null;
|
|
acceptedSelectionIds = null;
|
|
}
|
|
return undefined;
|
|
}
|
|
: undefined,
|
|
children: item.children ? bindMenuTarget(item.children, target, selectionIds) : undefined,
|
|
}));
|
|
}
|
|
|
|
function buildContextMenu(node: TreeNode): ContextMenuItem[] {
|
|
const previousNode = activeNode.value;
|
|
const menuContext = createSidebarMenuContext(node, connectionStore.selectedTreeNodeIds, databaseTypeForNode(node));
|
|
acceptedSelectionIds = menuContext.selectedNodeIds;
|
|
activateRuntimeNode(node);
|
|
claimTreeItemDialogOwnership();
|
|
ensureDangerDialogRouting();
|
|
routeTreeItemDialogController();
|
|
let rawItems: ContextMenuItem[];
|
|
try {
|
|
rawItems = treeItemMenuItems();
|
|
} finally {
|
|
acceptedSelectionIds = null;
|
|
}
|
|
// Normalization is intentionally evaluated only when a menu opens. Besides
|
|
// parity tests, it provides deterministic action identifiers for diagnostics.
|
|
normalizeSidebarMenuDescriptors(menuContext, rawItems);
|
|
const items = bindMenuTarget(rawItems, menuContext.target, menuContext.selectedNodeIds);
|
|
activateRuntimeNode(previousNode);
|
|
return items;
|
|
}
|
|
|
|
function handleRowClick(node: TreeNode, clickDetail: number) {
|
|
const requestId = beginNavigationRequest();
|
|
activateRuntimeNode(node);
|
|
runRowClickAction(clickDetail, requestId);
|
|
}
|
|
|
|
function handleRowDoubleClick(node: TreeNode, event: MouseEvent) {
|
|
// Direct-navigation rows already handle every click in the click sequence.
|
|
// Starting a new request for their trailing dblclick would invalidate the
|
|
// still-pending single-click navigation without providing a replacement in
|
|
// single-click activation mode.
|
|
if (isDirectNavigationTreeNode(node.type)) {
|
|
activateRuntimeNode(node);
|
|
return;
|
|
}
|
|
beginNavigationRequest();
|
|
activateRuntimeNode(node);
|
|
onDoubleClick(event);
|
|
}
|
|
|
|
function handleRowKeydown(node: TreeNode, event: KeyboardEvent) {
|
|
beginNavigationRequest();
|
|
activateRuntimeNode(node);
|
|
onKeydown(event);
|
|
}
|
|
|
|
function openPrimaryVisibleFilter(node: TreeNode) {
|
|
activateRuntimeNode(node);
|
|
if (currentDatabaseType() === "nacos") {
|
|
openVisibleNacosNamespacesDialog();
|
|
return;
|
|
}
|
|
openVisibleDatabasesDialog();
|
|
}
|
|
|
|
function openVisibleNacosNamespacesDialog() {
|
|
const node = activeNode.value;
|
|
if (node.type !== "connection" || !node.connectionId || connectionStore.getConfig(node.connectionId)?.db_type !== "nacos") return;
|
|
emit("open-visible-nacos-namespaces", node);
|
|
}
|
|
|
|
function openDataInNewTab(node: TreeNode) {
|
|
activateRuntimeNode(node);
|
|
openDataInNewTabImmediately(node);
|
|
}
|
|
|
|
function requestPaste(node: TreeNode): boolean {
|
|
activateRuntimeNode(node);
|
|
return requestPasteTreeClipboard();
|
|
}
|
|
|
|
function toggleNode(node: TreeNode) {
|
|
activateRuntimeNode(node);
|
|
void toggle(beginNavigationRequest());
|
|
}
|
|
|
|
defineExpose({
|
|
buildContextMenu,
|
|
handleRowClick,
|
|
handleRowDoubleClick,
|
|
handleRowKeydown,
|
|
openPrimaryVisibleFilter,
|
|
openDataInNewTab,
|
|
openDdlForSelection,
|
|
requestPaste,
|
|
toggleNode,
|
|
});
|
|
</script>
|
|
|
|
<template />
|