From 40a31db7b52b51879b27e15ecd607428d57ede10 Mon Sep 17 00:00:00 2001 From: tashda Date: Thu, 9 Apr 2026 10:04:26 +0200 Subject: [PATCH 1/3] Echo: Major update to UI, Core, and Features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core & Database: - Integrated new raw SQL Server execution transcript details for deeper query debugging. - Added 'QueryStatementClassifier' for intelligent statement-level analysis. - Introduced 'PostgresTerminalLauncher' for native psql integration. - Enhanced 'DatabaseProtocols' and 'QueryResultsModels' to support richer metadata. UI & Design System: - Refactored 'SignedInAccountCard' into modular components (+Bindings, +Components, +Sheets). - Replaced specialized tab factory with a modular system (Monitor, Security, ServerTools). - Extracted 'FolderEditorSheet' into logically separated extensions. - Added 'ExperimentalObjectBrowser' — a high-performance sidebar using NSOutlineView. - Integrated high-fidelity database icons for SQL Server, Postgres, MySQL, and SQLite. - Added 'TableStructureToolbarItem' and new coordinator for table structure editing. Features: - Sync & Account: Major refactor of SyncEngine including initial sync logic and merge strategy sheets. - Object Browser Cache: Added 'ObjectBrowserCacheStore' for persistent metadata caching. - Table Structure Editor: Significant refactor of the editor and its sheets (+Draft, +Logic, +Review). - Preferences: Enhanced Notification and Cache settings views. - Data Migration: Added SQL generation and action logic to the migration wizard. Tests & Documentation: - Updated README with modern design and architectural overview. - Added comprehensive unit tests for Apple Sign-In, Object Browser Cache, and Statement Classification. - Fixed metadata freshness logic in 'ConnectionSessionTests'. Dependencies: - Updated 'Package.resolved' to consume the latest 'postgres-wire' and 'sqlserver-nio' revisions. --- .../xcshareddata/swiftpm/Package.resolved | 8 +- .../MicrosoftSQLServer.imageset/Contents.json | 23 + .../sql25Icon 1.png | Bin 0 -> 43638 bytes .../sql25Icon 2.png | Bin 0 -> 43638 bytes .../MicrosoftSQLServer.imageset/sql25Icon.png | Bin 0 -> 43638 bytes .../MySQL.imageset/Contents.json | 23 + .../MySQL.imageset/mysql 1.png | Bin 0 -> 22577 bytes .../MySQL.imageset/mysql 2.png | Bin 0 -> 22577 bytes Echo/Assets.xcassets/MySQL.imageset/mysql.png | Bin 0 -> 22577 bytes .../PostgreSQL.imageset/Contents.json | 23 + .../PostgreSQL-Logo.wine 1.png | Bin 0 -> 163959 bytes .../PostgreSQL-Logo.wine 2.png | Bin 0 -> 163959 bytes .../PostgreSQL-Logo.wine.png | Bin 0 -> 163959 bytes .../SQLite.imageset/Contents.json | 23 + .../SQLite.imageset/SQLite 1.png | Bin 0 -> 16731 bytes .../SQLite.imageset/SQLite 2.png | Bin 0 -> 16731 bytes .../SQLite.imageset/SQLite.png | Bin 0 -> 16731 bytes .../DatabaseEngine/DatabaseProtocols.swift | 7 + .../MSSQLDedicatedQuerySession+Queries.swift | 36 +- ...SQLServerExecutionResult+RawMessages.swift | 48 + .../SQLServerSessionAdapter+Queries.swift | 35 +- .../MySQL/Modules/MySQLSession+Queries.swift | 21 +- .../Dialects/MySQL/MySQLToolLocator.swift | 15 +- .../Postgres/Modules/PostgresDatabase.swift | 24 +- .../Postgres/PostgresTerminalLauncher.swift | 72 + .../DatabaseEngine/QueryResultsModels.swift | 17 +- .../QueryStatementClassifier.swift | 46 + .../Domain/AppleSignInCoordinator.swift | 62 +- .../Features/Account/Domain/AuthState.swift | 59 +- .../Account/Domain/SupabaseAuthBackend.swift | 75 +- .../Sync/E2E/E2EEnrollmentManager.swift | 27 +- .../Features/Account/Sync/SyncAdapter.swift | 21 +- .../Account/Sync/SyncCheckpointStore.swift | 4 + .../Features/Account/Sync/SyncClient.swift | 76 +- .../Account/Sync/SyncEngine+InitialSync.swift | 27 + .../Features/Account/Sync/SyncEngine.swift | 194 ++- .../Features/Account/Sync/SyncScheduler.swift | 32 +- .../Features/Account/Sync/SyncTypes.swift | 77 +- .../Views/CredentialConflictSheet.swift | 83 + .../Account/Views/E2EEnrollmentView.swift | 192 ++- .../Account/Views/E2ERecoveryView.swift | 48 + .../Account/Views/E2EUnlockView.swift | 73 +- .../Views/SignedInAccountCard+Bindings.swift | 104 ++ .../SignedInAccountCard+Components.swift | 121 ++ .../SignedInAccountCard+DetailSections.swift | 135 ++ .../SignedInAccountCard+DetailSheet.swift | 150 ++ .../SignedInAccountCard+SyncSections.swift | 124 ++ .../Account/Views/SignedInAccountCard.swift | 478 +----- .../Views/SyncMergeStrategySheet.swift | 116 ++ .../Domain/ActivityMonitorViewModel.swift | 1 + .../ActivityMonitorSharedComponents.swift | 4 +- .../Views/MySQL/MySQLActivityIO.swift | 3 + .../Views/MySQL/MySQLActivityInnoDB.swift | 3 + .../Views/MySQL/MySQLActivityQueries.swift | 3 + .../MySQL/MySQLActivityReplication.swift | 3 + .../Views/MySQL/MySQLActivityWaits.swift | 3 + .../Postgres/PostgresActivityBGWriter.swift | 4 + .../PostgresActivityConfiguration.swift | 4 + .../Postgres/PostgresActivityIOStats.swift | 4 + .../PostgresActivityMonitorView.swift | 10 +- .../PostgresActivityPreparedTxns.swift | 4 + .../Views/Postgres/PostgresActivityWAL.swift | 4 + .../Features/AppHost/Domain/AppDirector.swift | 20 +- .../AppHost/Domain/State/AppState.swift | 12 + .../State/EnvironmentState+Connections.swift | 38 + .../EnvironmentState+TabManagement.swift | 18 + .../Domain/State/EnvironmentState.swift | 21 +- .../Domain/State/NavigationStore.swift | 7 + .../AppHost/EchoApp+ConnectMenu.swift | 32 +- .../ConnectionDashboardView+Actions.swift | 6 + .../ConnectionDashboardView.swift | 8 +- .../ServerConnectionPlaceholderView.swift | 15 +- .../TabOverview/TabOverviewServerGroup.swift | 8 +- .../WorkspaceContentView.swift | 38 +- ...spaceTabContainerView+BatchExecution.swift | 7 + .../WorkspaceTabContainerView+Execution.swift | 111 +- ...BreadcrumbToolbarContent+ConnectMenu.swift | 6 +- ...dcrumbToolbarContent+ConnectionsMenu.swift | 6 +- .../Toolbar/ConnectionToolbarMenuItems.swift | 2 +- .../Popovers/ConnectionsMenuBuilder.swift | 8 +- .../ConnectionsPopoverController.swift | 24 +- .../Popovers/ConnectionsPopoverView.swift | 10 +- .../TableStructureToolbarItem.swift | 155 ++ .../WorkspaceToolbarItems+Connections.swift | 24 +- .../WorkspaceToolbarItems+PreviewData.swift | 15 +- .../WorkspaceToolbarItems+Recents.swift | 6 +- .../WorkspaceToolbarItems.swift | 6 + .../ConnectionSession+MetadataFreshness.swift | 99 ++ .../ConnectionSession+SchemaLoading.swift | 7 +- ...ConnectionSession+TabFactory+Monitor.swift | 144 ++ ...onnectionSession+TabFactory+Security.swift | 142 ++ ...ectionSession+TabFactory+ServerTools.swift | 264 ++++ ...ectionSession+TabFactory+Specialized.swift | 536 ------- .../Domain/ConnectionSession.swift | 1 + .../SavedConnection+ObjectBrowserCache.swift | 21 + .../Domain/SavedConnection.swift | 15 +- .../ConnectionEditorView+Actions.swift | 7 +- .../ConnectionEditorView+Detail.swift | 5 +- .../ManageConnectionsView+Projects.swift | 2 +- .../Sidebar/ConnectionSidebarItemViews.swift | 7 +- ...DataMigrationWizardViewModel+Actions.swift | 124 ++ ...grationWizardViewModel+SQLGeneration.swift | 283 ++++ .../Domain/DataMigrationWizardViewModel.swift | 393 +---- .../GenerateScripts/Views/DACWizardView.swift | 4 +- .../Cache/ObjectBrowserCacheModels.swift | 28 + .../Cache/ObjectBrowserCacheStore.swift | 137 ++ .../Discovery/SchemaDiscoveryEngine.swift | 27 +- .../ExperimentalObjectBrowserNode.swift | 178 +++ ...ExperimentalObjectBrowserOutlineView.swift | 387 +++++ ...rimentalObjectBrowserRowView+Helpers.swift | 75 + .../ExperimentalObjectBrowserRowView.swift | 583 +++++++ ...bjectBrowserSidebarView+ContextMenus.swift | 1368 +++++++++++++++++ ...tBrowserSidebarView+DatabaseSections.swift | 139 ++ ...mentalObjectBrowserSidebarView+Focus.swift | 121 ++ ...talObjectBrowserSidebarView+Overlays.swift | 417 +++++ ...talObjectBrowserSidebarView+Security.swift | 203 +++ ...lObjectBrowserSidebarView+ServerData.swift | 312 ++++ ...ExperimentalObjectBrowserSidebarView.swift | 403 +++++ ...tBrowserSidebarViewModel+Persistence.swift | 56 + ...imentalObjectBrowserSidebarViewModel.swift | 271 ++++ ...bjectBrowserSnapshot+DatabaseHelpers.swift | 42 + ...jectBrowserSnapshot+DatabaseSections.swift | 219 +++ ...mentalObjectBrowserSnapshot+Security.swift | 328 ++++ ...ObjectBrowserSnapshot+ServerSections.swift | 272 ++++ .../ExperimentalObjectBrowserSnapshot.swift | 220 +++ .../Components/SidebarMenuView+Content.swift | 2 + .../Views/Components/SidebarMenuView.swift | 9 + ...ctBrowserSidebarView+DatabaseContent.swift | 29 +- ...owserSidebarView+DatabaseContextMenu.swift | 12 +- ...tBrowserSidebarView+DatabaseSections.swift | 2 +- ...bjectBrowserSidebarView+ContextMenus.swift | 3 +- .../SearchSidebarView+Actions.swift | 19 +- .../Preferences/Domain/GlobalSettings.swift | 36 +- ...ApplicationCacheSettingsView+Actions.swift | 21 + ...pplicationCacheSettingsView+Bindings.swift | 15 + ...pplicationCacheSettingsView+Sections.swift | 11 + .../ApplicationCacheSettingsView.swift | 11 + .../DatabasesSettingsView+Bindings.swift | 20 - ...tabasesSettingsView+PostgresSettings.swift | 58 +- .../NotificationSettingsView+Bindings.swift | 64 + .../NotificationSettingsView+Components.swift | 96 ++ .../Views/NotificationSettingsView.swift | 70 +- .../ResultGridColorSettingsSection.swift | 2 +- .../Views/QueryBuilderTableNode.swift | 11 +- .../PSQL/PSQLTabViewModel+MetaCommands.swift | 7 +- .../Domain/PSQL/PSQLTabViewModel.swift | 4 +- .../QueryEditorState/QueryEditorState.swift | 1 + .../TableStructureEditorViewModel+Logic.swift | 15 +- .../TableStructureEditorViewModel.swift | 28 +- .../Formatting/SQLFormatter.swift | 8 + .../PostgresExtensionStructureView.swift | 4 +- .../Section/QueryResultsSection+Logic.swift | 16 +- .../Spatial/SpatialBrowserLinkBuilder.swift | 6 +- .../CheckConstraintEditorSheet+Draft.swift | 13 +- .../Sheets/CheckConstraintEditorSheet.swift | 5 +- .../Sheets/ColumnEditorSheet+Draft.swift | 46 +- .../Sheets/ColumnEditorSheet.swift | 5 +- .../Sheets/ForeignKeyEditorSheet+Draft.swift | 28 +- .../Sheets/ForeignKeyEditorSheet.swift | 5 +- .../Sheets/IndexEditorSheet+Draft.swift | 19 +- .../Sheets/IndexEditorSheet.swift | 5 +- .../Sheets/PrimaryKeyEditorSheet+Draft.swift | 17 +- .../Sheets/PrimaryKeyEditorSheet.swift | 5 +- .../Sheets/StructureApplyReviewSheet.swift | 153 ++ .../Sheets/StructureScriptPreviewSheet.swift | 53 + .../UniqueConstraintEditorSheet+Draft.swift | 17 +- .../Sheets/UniqueConstraintEditorSheet.swift | 5 +- ...bleStructureEditorView+ColumnActions.swift | 7 +- ...TableStructureEditorView+Constraints.swift | 29 +- ...StructureEditorView+ConstraintsTable.swift | 8 + ...TableStructureEditorView+ForeignKeys.swift | 29 +- .../TableStructureEditorView+Indexes.swift | 39 +- .../TableStructureEditorView+Layout.swift | 65 +- ...leStructureEditorView+SheetModifiers.swift | 152 -- .../TableStructureEditorView.swift | 71 +- .../TableStructureSheetCoordinator.swift | 46 + .../TableStructureSheetHost.swift | 413 +++++ .../Views/DiagramAnnotationView.swift | 6 +- ...gresAdvancedObjectsViewModel+Actions.swift | 861 ----------- ...dvancedObjectsViewModel+AlterActions.swift | 227 +++ ...dObjectsViewModel+AlterObjectActions.swift | 250 +++ ...vancedObjectsViewModel+CreateActions.swift | 189 +++ ...AdvancedObjectsViewModel+DropActions.swift | 206 +++ .../Components/DatabaseTypeIcon.swift | 58 + .../Components/MSSQLDataTypePicker.swift | 38 +- .../Components/SidebarConnectionHeader.swift | 93 +- .../Components/SymbolLikeAssetImage.swift | 82 + .../Shared/DesignSystem/LayoutToken.swift | 29 + .../Notifications/NotificationCategory.swift | 87 ++ .../NotificationPreferences.swift | 69 +- .../NSImage+DatabaseIcons.swift | 48 + .../Modals/FolderEditorSheet+Components.swift | 60 + .../FolderEditorSheet+Credentials.swift | 104 ++ .../UI/Modals/FolderEditorSheet+Logic.swift | 83 + .../Modals/FolderEditorSheet+Sections.swift | 74 + .../Sources/UI/Modals/FolderEditorSheet.swift | 369 +---- .../Components/MSSQLDataTypePickerTests.swift | 22 + .../Models/GlobalSettingsExtendedTests.swift | 83 +- .../TableStructureEditorModelsTests.swift | 6 + .../AppleSignInCoordinatorTests.swift | 31 + .../ObjectBrowserCacheStoreTests.swift | 102 ++ .../QueryStatementClassifierTests.swift | 34 + .../Services/SyncStartupDecisionTests.swift | 54 + EchoTests/Stores/ConnectionSessionTests.swift | 34 + README.md | 121 +- proxmox_mcp.log | 6 + 206 files changed, 12782 insertions(+), 3651 deletions(-) create mode 100644 Echo/Assets.xcassets/MicrosoftSQLServer.imageset/Contents.json create mode 100644 Echo/Assets.xcassets/MicrosoftSQLServer.imageset/sql25Icon 1.png create mode 100644 Echo/Assets.xcassets/MicrosoftSQLServer.imageset/sql25Icon 2.png create mode 100644 Echo/Assets.xcassets/MicrosoftSQLServer.imageset/sql25Icon.png create mode 100644 Echo/Assets.xcassets/MySQL.imageset/Contents.json create mode 100644 Echo/Assets.xcassets/MySQL.imageset/mysql 1.png create mode 100644 Echo/Assets.xcassets/MySQL.imageset/mysql 2.png create mode 100644 Echo/Assets.xcassets/MySQL.imageset/mysql.png create mode 100644 Echo/Assets.xcassets/PostgreSQL.imageset/Contents.json create mode 100644 Echo/Assets.xcassets/PostgreSQL.imageset/PostgreSQL-Logo.wine 1.png create mode 100644 Echo/Assets.xcassets/PostgreSQL.imageset/PostgreSQL-Logo.wine 2.png create mode 100644 Echo/Assets.xcassets/PostgreSQL.imageset/PostgreSQL-Logo.wine.png create mode 100644 Echo/Assets.xcassets/SQLite.imageset/Contents.json create mode 100644 Echo/Assets.xcassets/SQLite.imageset/SQLite 1.png create mode 100644 Echo/Assets.xcassets/SQLite.imageset/SQLite 2.png create mode 100644 Echo/Assets.xcassets/SQLite.imageset/SQLite.png create mode 100644 Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/SQLServerExecutionResult+RawMessages.swift create mode 100644 Echo/Sources/Core/DatabaseEngine/Dialects/Postgres/PostgresTerminalLauncher.swift create mode 100644 Echo/Sources/Core/DatabaseEngine/QueryStatementClassifier.swift create mode 100644 Echo/Sources/Features/Account/Sync/SyncEngine+InitialSync.swift create mode 100644 Echo/Sources/Features/Account/Views/CredentialConflictSheet.swift create mode 100644 Echo/Sources/Features/Account/Views/SignedInAccountCard+Bindings.swift create mode 100644 Echo/Sources/Features/Account/Views/SignedInAccountCard+Components.swift create mode 100644 Echo/Sources/Features/Account/Views/SignedInAccountCard+DetailSections.swift create mode 100644 Echo/Sources/Features/Account/Views/SignedInAccountCard+DetailSheet.swift create mode 100644 Echo/Sources/Features/Account/Views/SignedInAccountCard+SyncSections.swift create mode 100644 Echo/Sources/Features/Account/Views/SyncMergeStrategySheet.swift create mode 100644 Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/TableStructureToolbarItem.swift create mode 100644 Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+MetadataFreshness.swift create mode 100644 Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Monitor.swift create mode 100644 Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Security.swift create mode 100644 Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+ServerTools.swift delete mode 100644 Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Specialized.swift create mode 100644 Echo/Sources/Features/ConnectionVault/Domain/SavedConnection+ObjectBrowserCache.swift create mode 100644 Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel+Actions.swift create mode 100644 Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel+SQLGeneration.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheModels.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserNode.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserOutlineView.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserRowView+Helpers.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserRowView.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+ContextMenus.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+DatabaseSections.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Focus.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Overlays.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Security.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+ServerData.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarViewModel+Persistence.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarViewModel.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+DatabaseHelpers.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+DatabaseSections.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+Security.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+ServerSections.swift create mode 100644 Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot.swift create mode 100644 Echo/Sources/Features/Preferences/Views/NotificationSettingsView+Bindings.swift create mode 100644 Echo/Sources/Features/Preferences/Views/NotificationSettingsView+Components.swift create mode 100644 Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/StructureApplyReviewSheet.swift create mode 100644 Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/StructureScriptPreviewSheet.swift delete mode 100644 Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+SheetModifiers.swift create mode 100644 Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureSheetCoordinator.swift create mode 100644 Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureSheetHost.swift delete mode 100644 Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+Actions.swift create mode 100644 Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+AlterActions.swift create mode 100644 Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+AlterObjectActions.swift create mode 100644 Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+CreateActions.swift create mode 100644 Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+DropActions.swift create mode 100644 Echo/Sources/Shared/DesignSystem/Components/DatabaseTypeIcon.swift create mode 100644 Echo/Sources/Shared/DesignSystem/Components/SymbolLikeAssetImage.swift create mode 100644 Echo/Sources/Shared/PlatformBridge/NSImage+DatabaseIcons.swift create mode 100644 Echo/Sources/UI/Modals/FolderEditorSheet+Components.swift create mode 100644 Echo/Sources/UI/Modals/FolderEditorSheet+Credentials.swift create mode 100644 Echo/Sources/UI/Modals/FolderEditorSheet+Logic.swift create mode 100644 Echo/Sources/UI/Modals/FolderEditorSheet+Sections.swift create mode 100644 EchoTests/Components/MSSQLDataTypePickerTests.swift create mode 100644 EchoTests/Services/AppleSignInCoordinatorTests.swift create mode 100644 EchoTests/Services/ObjectBrowserCacheStoreTests.swift create mode 100644 EchoTests/Services/QueryStatementClassifierTests.swift create mode 100644 EchoTests/Services/SyncStartupDecisionTests.swift diff --git a/Echo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Echo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index be236576d..abc6344c5 100644 --- a/Echo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Echo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -43,7 +43,7 @@ "location" : "https://github.com/tashda/postgres-wire", "state" : { "branch" : "dev", - "revision" : "07840f432d33964c6330448ab1562f026d103cd1" + "revision" : "b4375fee0985ce8c60e40245cfcf09af4bed572d" } }, { @@ -70,7 +70,7 @@ "location" : "https://github.com/tashda/sqlserver-nio", "state" : { "branch" : "dev", - "revision" : "4663b84bcc8a630ebf2510b68daad7708aab801f" + "revision" : "cd1cf5e220a49e7e1561bbab8e0851342c7c51fd" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/supabase/supabase-swift", "state" : { - "revision" : "ed9042e60b57257e531331da365955a5c0568b12", - "version" : "2.43.0" + "revision" : "17261e93c60aa721e3c17312bfeb2ae6de3d6f8a", + "version" : "2.43.1" } }, { diff --git a/Echo/Assets.xcassets/MicrosoftSQLServer.imageset/Contents.json b/Echo/Assets.xcassets/MicrosoftSQLServer.imageset/Contents.json new file mode 100644 index 000000000..21539d66e --- /dev/null +++ b/Echo/Assets.xcassets/MicrosoftSQLServer.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sql25Icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sql25Icon 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sql25Icon 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Echo/Assets.xcassets/MicrosoftSQLServer.imageset/sql25Icon 1.png b/Echo/Assets.xcassets/MicrosoftSQLServer.imageset/sql25Icon 1.png new file mode 100644 index 0000000000000000000000000000000000000000..bbfed20282db428b5c53ba41d1396f26e75866a6 GIT binary patch literal 43638 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%0D0g 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 z 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 literal 0 HcmV?d00001 diff --git a/Echo/Assets.xcassets/PostgreSQL.imageset/PostgreSQL-Logo.wine 2.png b/Echo/Assets.xcassets/PostgreSQL.imageset/PostgreSQL-Logo.wine 2.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe687a7304ab13a30bb3b3817dcd690b82b666b GIT binary patch literal 163959 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%0D0g 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 z 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 literal 0 HcmV?d00001 diff --git a/Echo/Assets.xcassets/PostgreSQL.imageset/PostgreSQL-Logo.wine.png b/Echo/Assets.xcassets/PostgreSQL.imageset/PostgreSQL-Logo.wine.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe687a7304ab13a30bb3b3817dcd690b82b666b GIT binary patch literal 163959 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%0D0g zxvy2w>8H$x58ub-=jxlC_T_#p*3k(IYv0}7VFct1GlqJ70N8_OsCt8Ss`h43!3dY> zxU8acvVqLV_;||lvbKQNO|7ws39&obbMIw#!7O37NzE*NyRP|^w6tgQ!CU%?J$pv| z(P)v0mbA(*z~!;KgtbrA&C012vaDIbx>s6!bG}7UR%nf|y$A7^^Aq*C+Z}&*TZf5) z_9ybr)JB;d!pq>|i?K5%c@UeIwMNsMAl=P>tr9Lr$;c@4`Sa&5;sGeHD|AK7<|{u{ z>9%`&nN1hQ84v|lVWUW-bXI?B6+|&5?op z`NA6=v!ty8Ji&8Y6TfNfuZ|65Wo0=4e)EJpetX+kz;@mDV6A;^zW>L(o|_~sLwZWsC6w1L$Aj-3^H13H4!Zv8Ok zoY?(#1uwp;>2n*)qzAM^$pq0{(?F47q-wETc3J3;D;Wdjc<-XuD*V?@bL1{Mrw}`_ z!ydAZKmYl~W3!d!I&PcQW%eh{kwKWbBw>0^EssqdNRV9Vx>6gxZk-ouOk)mKTi?aU z$7A>r(qKl^-%C?=?a(D%s-}i4)lXTr`%SohXcwrP3N7B(k7a)t2N3_tOYJX@6{Yi) znjc8v*D(1sgn~~*w7{yElYZzfWfF<*J@-!p>Me)oo)mboLZxb2Q z_=RILGBYRYe03hIw^}mZpDn948746D>W?PphO0xqG{pqroRBAJsD6;*zSLF`q7yU1 zXh(TZ>#H5yuHmMfS4E8fMG=8|%2FUc?_;?@DjbyFM{rAO84ixuI~@4CUv~KXfr;RS zV9Q|yP(}u_Ds=%5l9Q4OwCWr1OHBb4%XQ~K{lx92@?XWr#_Hoaa#YMtm*Rn4k)p77 z8&ub$LT}}GVeE7WWENnFRvp?g35~7qgx^^Uo?3K>C;0B5Sx(|KVuTGlu9yi}ft^Mg zN`%Y1Ql9@cp^lFb<)lW*kSD=$*nM5rE6v2&DtW0Xj^J^5Wc@mS$psuNE-oIdf{oG@O)dmbd3KV?gm0=-QYLV5 zN+zaZc3OIR`k`XA664X2JpP|qEb|$}Xg>ca7)V2(bp1Z!#@Ah#UIHtubqhhq-Agx+ zE_?)7WC7Zj@5t|qMpU)F7@}545n(=6zOF%!3jSxTEf9UigUP&zjTcZQ37L{vE6vKD zmN(L)%L3tN({NE{Iq9&@Zj%Cac^w%tI&~PW%BR0kvlN7 z-s!3K6{=pA!CM6XGTTToyNVv-;$G)$v)4_Jr=}|Hm;d^Q6e#7vyb0sMVqJ;j;j-4- z1~ZzOlB(*elziWmDv)0nm#N8q@@TdFj$AjF-N|O}Z$<947OKE&x+FWev+IeaXxDV1 zHsI%(3F#@eQNZ`LG3`YdY8#hhm|_daz-Y^$wEG|Qr2riP8M57w}C}M`Uq%Yny0ffdM0$ODsXN7Ic zaZ65G5@D(c5dWa>0ysFrrcEK#G&Goxs3HTsZZCK+g8lri(hLFTW6OsWC|IFcC6Uag zXU&XrII{Z&%s6)G1D6TtmImtr9PQJbvd~Wn9>>~-qC2&`yjbzCj;U$k`3wkeHNIUJ%${qSU^hna=jc z30>*O()pA)IY*A8r(UvqdMxa?Uo)s;S9M8)DFFWVsUJt^Cm6%jVcOB567fAs--gjO zfaJ*jK0XgIdw5X)@qJ^z(gPOM7|v4+pyM-UW;|x4Y*7{f$8WSw&(5N(7?iRSxlG3f!f+Y3mzx~? z!#`ttihAEQk_kDFFoM=^w3U7FaxK-V);S3-eoXZr!&^>=n4*qy)qWRbA@n8Fb3T59 zE$Qx9XaIE!?sfY9xeZ7dJC0(+{JN&~NjPCCLV_#li!ptW>ew-p8OQUc`MWG00L^<{ zE*vKBxT14y(`=^4R-|3j_1+FVfJ{hE&Jsn!6YqZhQx_A4Wz-xG|y-NL5?QEvuwN7`RI=VBd3Pd1BU(c*D-E=l&xPOk;4g z+M1`=?%joLEdPosiOw$@ZF_s0$PKnPl15YWeiXJVe@7fupy56JI8wt+fo^4r(CA@| z7FZ6hy%n9@;0bKCgg6JkjlsjuL*A@^LH?LBBQw)$!l*F-LMGRd#9-$i?UN;Ef?m!& zafbcJz5)FR3(sGTBr@PtIxmlefRUkL;(RNYdY?l**E_kKj zb(7}Qr2C#|1GcYUB`~UB$~Dr+0APP|VFV^&;y6bbJ~@Q=&?qN4KK!|_HXWzv?ncSJ zJY2ExeE)rwO)ol`<=s$@c!2Todgo-jj}OhreM+k~!fwGxuCCp&{6>8kG^W&(Zz|m) zK@zDrLnEKz6p{xm(PqMqs~H+jJ&)=h1=cu~nZFcX%Ydw}pkfgOR`Hlk_6)cMRq_Tl zQK1r?zBM2pw&fvE*T4c7)IQ~*T)#R|plKk841?N8#(!|lT@U{LeImCHmW>13TRg%O zB`bJ_wmwG27kw-vwX?e!EY$*Seu->TD5*NHAO>PHT&%ZZA{TUw9m^Dw2cS91>F!T; zS_0?~ulDOMa-Kh|l*U-WFI?e=%S~?pP^A}gKB)9kWN*<7)Yyonl8mH_Tth4|?K-tD zSv_O@>HX!}@E6$xO~+p3zI*u=1#i`$YwVTrSv!$r+mbDQ6f)auXzfh*p91?=*H89T z-qV7*^&<#aFl>_gqDbXcRTCSNF-dv3XPGvCnf7sfe8mvhPA>G(nh1t-vkMFCA{X`4 zRIZ8^s2}0AsGmU%?s*1JP>U|2{1U|GkLS-uBEh#*Yes``*Z>Xb&aDJ!v;UeQuLrJg z!QHCJSImG-V2LmS07j;c6%_2UQ8%Z{)E$DAGR)S#%vf)9l(zjHu=Hhd(B)7B0c-4F z0>f17_a`}o7k(M@Mrz8Cn7k^s?QWJGz|0H;#09P@8OepvKi#Py>%uMR89){Ifz=!I z6rZe)-}<2%l&F_oDVuLLNfPm3grhzoW4E{He1qInr%h!GjWBWz31f&YjuRt>94}!= z8pKeDMAD&49hHl1K>=?YgUblYJsI))*U}t2!4p#6k;ZY` z^+dY?VD_zP*EwkwGx$~V6*J{xr1+|5%Juns9PT~fhw$nGohpGKfPgEe{Rof2?NtWS zGgu1By-It;S@i2eKFCT*ikX7M(h@|N#EZLTr)~RBKYfGx1*qWkNonY^%uPmOrL^Vt zG9~yPK|2cl33*q(x}=DD-4LN9*IRALVOX7KCbv|pLDvo3H6|6qVqzY&c&(SeClJc9 z`(u#{P7mln1E?_9)vpOulwg$3UQ^OqQ@s9&%X$3yDi%bvI&4;UniD&B{0cu1 zUUQuaIz^$n#Qy?dU+^bv*|Ru@*wHjdtlr}?*XXjEy|F>@^S6QhpXJwE!A4sEFngWG zUMD2qJx80*2B9Z+lwi*op<)nG0``lq9#i+wa%&$5^-^4Hl7zuvk?8ry%7Nk&xIwMQ z)%GK)9Gr$d&>s^uge5Mw_M2{2Fk42X7Rqcc(`e6|9|fEq7nX0_jow+BA9J6y4Xvpu ze~5{K3?WuOEtJej%z>Q6ULVt8y6_dZX3NA&#wC*!o%hfd^fcpTDO_U%weXNGDy?=V z<|m&f5mtiF08+BS3-s0YI-mL7QOqxqwSfdameQ*-3}V)6TgKjgU8)cf8JRed`*Qdd z-wJK3$L00RaPiRZeuuyKqXWSXI~(O&8z9v4q$<&ycpB$CIW$Y^S52M!CeMYbgzt)baj-DOjz^6}(6tOGZt0Nf>gq`JoPmm+Vo11? zD|0maCDd{{Nhdk+lk1?1R&yQHEme*F2vGNXhHdlJ2G@d#GoL6IYt@WchcziRLCQUt_Y ziK(Ro@<(XgfV9%OI4)KeJ3*K7e3OHzA5t7Zm}Mv#7+%)ftXb3U_eT-K{i%gnX#!p> zj_~VE58gv!8*IPjh;tebU^nf4w79bGzYkr=#e#O&HhSN?Yt_7u0%9%k34m1mxj*t5 zW@%XNLLU@O#np`mlAAvvv2CV{&1aWnS*sLt;?HPX;YI{pSdfy!(F2Rbr zN$@?9K+Ot5IFkMHuZq;v)G|R3OC=E2%1nt`%{{+>7!Wp^VvA{V+0%Ico_P6sVH$Ic z(rH5Fa8l&5n7QRU&hmx~Am0hvEaD{J3MrA66WSjB z+;V!~xe{QN5(#ODC%rtcZ3)jD%Kaq=#KK?4EC;tF&bO6R-4k2V-!VymsQQnYdn8Q( zl*@BSVfA#-!|@fKorP#8iP=q|h=-D4gcAOOcBnvDy#OB}4wfRq)hK)0OWJtqyTxG*T<#{dX;HCm zJy%zcaeA!c{GZI=EcJ2j&D8yI_iJK4>kNfV!6Y-6AwDlwwx0I=jfEYdpqC4AfIoE3 zn9++VO7OnZ%-gTvJA31X*3;WNSZUB58C###>MmO+?>+R`5$)kTk&r5e#xDXUMyE^! zn=5-~%50!)tdbnDxfzWPE6jIWUZ?V2vT7h!GrsOB@CUi^|0~SRCn7?Gma{RT(^Zcn zS!Z4=!=2P!W07Fp&>fJ;oaBc;mHQ*%-&eGemeTbi{(X`jiY)e|-bE5N-S%S@!$Lc|={Cuv){Wv4l9I!2^jH8xIp+2| z>&_%((`81*qWJyWWTCwmN?F!(&QDuZ-sQ5uCs0nkJqXB^#97cUa5=V*lpe-syUWuK8 zRjXPmnN`Q!mGSc;S2`COf;Lik9*YwTS~gy(Jv=K5-*(No zs8LmW#G^dI&EAE1dn`mTz$}z}=q0@SfQxoKl2%Y~@tC#c{$eT5VQ(y}3F<)yj1cbk z`?n)a3-PBhfN*ejWsUzJHRknzK@nDl@zix4=~K!lk-gO1|0JpPa69hlPN};{3d`gCXMo@H;*HZ!{n@=qXBOp+3QbfeM|CwA>U(g}b zn&;UoNzM5RDO*#geIL~P{`p<-e6UV3 zB&_|Z`2a%3=Z_OpRE*Av=fVhQr~ll{6;Y|VmF~NCibEp}ZNaE$vCpZTY~|UFdi(E^ z`mR8?cXkH99cvu^7QHD6wu`oSS0!(k1cAQ*vE{^J zog``K;_R-pfeh-co8(P4kv$qaClSc7`&*~N+~+gQ@YT6UuG3nZS5(M|O0Kl5SO*&) zaaI!vVt-ZQB5l|D;ow}u_D0kD8wN!)v|B(DNO(}m1TTJ^7H%L2XMtu2cqnozZOhIM zzOiQ((%`f|rG0}eEkbmw4-qcf?|U5;RDLe3(_r!VNokSv3m>#H9r>TLMRZinrMV~r z&CM7^I#crmB&zaV0T!~Y-xh=8Cd--m%ZqV3+AC}rJZZ*-&oH7w2|s55+MJ3XN8Bh1 zqW813)Q1kt+z@-EF+}Y3>v#Y>O*c1tBhqM>L^S1p zAvc*lb=$`21{^Vw=Fh5SS%gbhWB}3VyU)rBcvC-^0!mwemN_=FHSLIC+L5EG8pZS1 zPiG!KNxU(a!N8g@k+CWwgnh{lpoI%-Qg@rn2Ob)Qe}U)LTcWa4d*9Evx&i#k1enTX z@(@%!rCCuG1(P(?oA)WY^kR<^Q~WeG>|gy}eg?;QqYkyRvpYTR#v(;l2K4q4-=qS> zt`++Mq<$OUpToOTp+`M4B^otXaz6&(wS+H}|D2|@4_Gyd#a#JXuCL47_xx)&XCUDrfXAL;mZx!k|VB52Q)&sD%pFdp3e96mOuyjnK z7^4<(h?H~B^H>iPr2|}<(n-qxrq5Kae%{N0YNI8dIc+0hK+e*vs&r&Eh$-hyk;YQt!*1721Xcabs{nR>Li4>5g+3AI{FswgG0Fn4;&c z#mUK8DnxW?mi1X@rL>Fv{UyiwTP1D^cTA@sw!lE8E!98D8(dcQ50pLaF)5O%(lW|rLPUW_jcJCdL=Ty`_j=qIp*PTwv zvkmT7w6iufHO;D*D#Dc<(7POA{4BA^9#ZI&RH2Sgs#_Za9&k^6WZV z=#brBzqhaNts|alIw0@;fW+3o0RajjiBOU9T7F}%b%^fv|2e26a-O*CFfKA;_Pudo z&sVlX7Xo-i%r8y} zH>FUkMdI5!LjcCPb-#FD@1#VxFx>HA=4L%2%hQay7G{5cFSGUO`h9+OabOo?^so&P z9v&=2gjf-?UOWMGPKIb;2%GJaJn37r-^zs=Y%^r*S8)eX}AA)xb%5o zV762XVtb;509HYyZBm8V<4sx~j6>m6R*&ftTRrpqFXo~WfT&L6C;d^fN=M<*4{Xw+ zNo5iIeU9u(VuoUwg1|sXx61pCt|0o^Nv7golO=c3bQ4S(TQ7>GeEYiL8xFUFS0QIX za=uF*ymlep-rn|eIxd62Mpi;(sKR*i%n`j@rvM`(BUc*_!qSA@stpus-?(ylPi=_& z8Q~f~=35cbBfJ2J;kr0D_!B$JoBF51aG{HW;fqH*_g14^KpO6_Z+7oGeo_N|K%|GodN_P4-g^eV5dJBdzAN^Y&=4{m(gD zfyVnkuTA$T@aZFX*5;l^ImwJf5)AS6{uop!DlZEtH;l5JK^J?V*Iw&;HD zRN|9rQK9Vg&J%RS!Zp{?z9of2OcgEEp~`k2mcDGYiJQbo_S^h4m~#ET^g~>?y9n;( zRT6wz4$xQ^x07DNYu_It+bu8v^n%bRt1cpmxiBO*C-dZYfV6Br$txL|P~ppE2m2$h zORk3aqW8f=fZ?hpg2Hnm^%5!9DpskEA%NgrfCB{pg~BR*z{Kivp3B#KSb6qBixTCt zXR;LDR|aW7n10D-9nJK`i=}RMPglR1ySg@RB-vkz_`uqK&a}7~ z=!PM3ot6q6DZxE}AT#*Rrj^d?=64l`!ILt6KmRVS*UGNyirbl-$yc77N3|Jn=4ov^ zQ`$>_JC)s@Jw~TfqSHI;&zv-Qk*;_C6Q(((SFBM&?}yYpeT%|eqV`qF-+Y$6`7vg( z3A4oyJDyrfv0ra_RKc|T-&E>$;o-^s8vY$83D>8ipQUKn45Pg-7i2mHA^Q9W%barV zy*x>O$kRFWKBu%{xVgCnKZ*{JOj~<1ja8I!0khZ5$3xVffcquC#$ubt%Y}yA@V#i5 z`Q7(`poe?eKDu39sn5Id5+o4Cv6iwO?}DWQ8M^~A*aAu)cqC-#F=(_Hlm@E~w2ed< zE)zSp;BSy`tDT;T8m*wOYJ{2>zA4F{R-vKB2&av zB?*qo8)h`S*V=&!zX0`}93@*PpFHLxc@PzLBEOIat5aj+YH8dQ;hdaCS4}>v|8x@H zFL(3ql}1PiC)?aLNnX5lom)+M8-Ot`(iL;VUS)>+b6bG?l+>{1{Z7#F`?^kZ3kiau z^Jn-D$6n27=mA4fp3_UC&}tz@_lg*YJ?}&#fhPO!n$cuq$^sv0o)Is^f3%Z$ z9sQmdMUt#LpU?Q`tANi)1HaA_#H;G30cuCxy)RQpLQXneMiXb?^nGD~Ey8^zE6$9m zevt5NlaA*M5(YdI4+lE4HX=%HxPAI$^h?UFTwp(cx+xS9SD${*jS=KX6nGJ)TL3#E zu0E&}1_&QB7TqP8L$))QVsHHj4Gql>(BC8uBkT9Nl9!&Q%q#MWx_-fR_#ZS$yH>o> z?U;l;W&!aym9%R~o66dBrb8uX$yWO$STvmP;CVqFj>SylaXxKMa5m%v^xX-;7hQU& zPrzOn)7>Z#KNt0rY#yD>dj72e;%*|t2xgOxLfQmZH9T^(+ z6IH(ZSl>*WO)ACdOI$f$@zXAC{li|XpR2axoTfVQo}3<;v<2jRb>*tjkd{}V%Y~G> zsAO@ScXAJkRV@`49T3XIYk;OCJph`J=(x`>zm23>lk&rs7bbFMsCL^0 zG_Jp7s)H0KxaX99weLvK{va?o_@kXn^dy==f=4av;82pdnvLg8R1k4B(@yH2)m@?R zfHc$ycpf`rmwc6$acU2DDbpxvFtJBF#>;RHQ95^V+MUhWx_;}ydRO;*y+Z|rNM|`rwyK+ zsN}uCD|CTMWNxeUt?Eb0lnxNj3-z^apkcC~Yt4)Hb5VBi*rq{+Jwie9iSL=_6 zg_aC|?p+bI=WuDD3mA1EcXnzI{+8f#6De0|!xxn4B?S*4NNN?GG$k@$qB zc^mugJMNX<8iKT$6RLaNOpRwger14udZYdz^9O~&%pj$ek5I^^B8OaiPcZ^VHk`W9S2xBrsd-jcj zLoRgBX_ssH)ano96Icdca$|5#O=d0WhJ4s~%o~{UnFROUi@B8gSJYo#CEQHPv=T0q z%7Ro#5>l+w)xPB-8wtf>l6p-~}wV~Dd=d`To{qHvj{%nK4F~dfwEc}4qwdic2 zxzzdF(*239*w7s*HvLMWZe?8UQ;JBH=2guz^A|4zNd)R$b4~sT)W!z`FZcu#Qnx%= z5Pk{j3%dFu8Ei_05&UjO^FxqaUlt7@wJ`Xu{h2kF3Z$xXaty3B*H$u|HmZ8dvs#@! zK2(~0a&(veBJ8yH4pV_ee^&OhZa(l9Op67p1NRl1UUvs#AlBh;J?qo1;uyRMmd&%)IK7hP5Pk8%2|=5|L7m8AaV_s@|3^)=Hzmm#8O#Vl1RzBH z)9%eGO&Gr{Qp0nXSyu)smUSjvHLX>51hIp=mDIfT%5zy9v0E{{__WF^>sbcPsNhX{I0LcmLLClk%C_D4LhD=`iFmYiDX#w^U7yiJF(aF37 z(AMeP8q}jqjI*6NE9D)6c~TOVu0y=;PWlnxlbpJCb7y3AOkAGKS2R-Fsi`0Y0jhxc zc=WHMheQtdCoe_RO4MKd4akAGhGE3&d*-#woFU!PMFHt`1#i(O+Yt9M_R>+IgJ0hG zfx+u#)?7TapGg>DSN-N~EZaJW%Hl#YBv)AZQW~{9>|&s}>{e^}i#?H>IzAd@5^`i1 zxgkxS#jx~bn-;Aeky>2QSEc8LZux5;>*WZn=jotqh!z;rL5Dga=hjSozb$OO*fQQ? zP^e^pTCg9EL!J-B)5(Y1)b9%xvpxTETCaopecui+k}TrNybbK83eoFrTw;FP91q5- z6d2kUlPq=$;f*|4c4C*D7v1YkX(d=kl-6U+Fbf=fYa%=YRm<}I(!f30p9f1rkW+4_ zo^Y1IYS%3at#bLc^{P|LMU6|%d-9|5RUrVVcrn5|{g`b{gBPTE&iUwB_|WOn#Gn2Y z1FKW|4t!)~AtjZTa;H(Z>>JAxVyO7Q(Nd7@y~r4?QB}j@_B*|pCw#UmbzoN-AYaX> zyFPX4?M($t4!=3we}8a0YQ3aoW*#9!UyzBwD6__3OTsi~l=?s_fk^*YLTnQV@F}QA zXJY5o6D}GeXFcD&#o>Nk5>OrIi;G72+je2stJ-o(RI5!uOEHtxks2j>;WGpqWxmj%!9lzXiiHF^3=^zIBVp|mTJ>$M(}xAUyS4dKT>Y4th4pXw|IZCzjEte-=W)m5%^-&Fb%)A>XPZ1$KF@$s3(?+sPBIi^W1FZ^ za)|P>@hg?Xl5=zR<&vORK(~t_>v@_jFh;mPZ_(2sxg7OO<)1omBq9vLo}>6{_%X)I zCvSn;@kdBRPlR}F7*pz_d4+MeYKb0WL|U%W`PQPY-B)Uf-guKUad{AmRJ?x#cAWsS zhmny@)io!tA=wLZ46)hSY!7#P&v$^t@RY|#hrFkj_pK3*P`<)%Z~9+34Eh+VJ-TKS zxgRWcGRUgg+T7euw`q%eLN1v4@>ztre@oD@f>yIxMI3wf3=fN7oa$F?h%y?tqJvdk zL|E(vN1XI3i#ucTV{$A`Dx|>6(&^=(8ug>tyf6=~^^QOXsU)C+C`-6?ktb!EM5aBGv1+c&3P{kU^`~QrFT-x1qE$t*HfcTyQv&T+bb?>N$czS4$t0G zS;fDO9-ItpJq;kia3NDL<+OY}O0MSg91#afZ+a8qZhhoc-ZF$ZFg8Es@^GZ_Yb{DU zz5htg>9xZyqB$njTVV?=k|3%PHYH8-mX6~wd^dbpB_UD^ zl-S7)HR*yGs@XUF-o#|k>vDxb&*$EW_~l#gOK0*<%B1e(r(rRuK!SCLe*L}3FM0go zGf8BVz-Mc~tw}T+>owSI1lf)<1lG%WDBWJnj+^U8;1FG2B;m-u4?%kuKR<=Sb7FVj zo;NkCJDSoU>EVbySzNGiAwiMMPduJsK5l-`fK84A_u}Ek?&}$2Awh==)wm^hbYXto zIX_CSa{p?|QR702dt9RXs{H4woHfOK8k^Wq>KroqsJZS?r z`4SI2x{zoq%neAj8LE2x+<2_&4|o1}9o@DW&SVTQG8j&$a^mU8lP;Ov(pT|K`+x|) zcygN2Wdy`&KndYzYpd2K3uc|SQnDJYgAr!R4Y1Z@JmB>@kp1}BR2Re6o=xJH#9Ts( z4i-R)!KVgwZts?5yRg=Z~=Yvl} zs8(hYn$s#9hl23VPEWT#g;2!Dh~9kf%DhK-xBBw;?JLBc{v2HfbCj@A?=!e8pGwX^ZX zMS2#?S11INtyH-(>pj!Aw==8v*#%f($Ip;lX{l?8DZ;#jdcM(2JgTX3zeRF;aYl7Y zSC6`0E&mcn%r0pO#U;Ul)vEWm0NqH-qpnmOLH^e!258-T`LBWZ@-)#i48MMM^oo+( zGF)PA2)_%qX}_;uw(BQPo?3Z_{&1B$aFhA$^w*@6R=yjC*2&nrZ8vfl<5R!&j*pYF z4%VHi^JPv(qegGC`Mh@|_ZoALbE3X&ZJ#mX1N<0f5i^&7t zajnGTfAJOU)FhW@v=H*~G8D+)q;Z?Y{|02<7P%W`u9TDwzMGuEFV~_OP~DFZxC{uZ z$ZFbmUaLF9;~#H7xs~US@yn4vGVr{!-!nvJFx49!+9Rd&LV{sget)D zkjmFlkj=ABbM_bi_pB!ikJ0#3#kwm6Nya>jA`fu;2`78oP_M7APp`IOh7mbj%aStn z@t+?KRj)_Xb5Gaxfe@D5+G#~Z%|!D@NScR@wsryc}H_TOXy^M-~>}yk-4mz^&bA*WkWYy7M^49%oCOcHIli z5$9xLdpT=wH9k98s2h5TU%7$5w|~Fy)2>Uc#c*cDFZd)3(jpJa3cbWQdZuAPF=syW zf!=PS!HtMZ;?1GTbq9|wE1RP41DtgX#iOrSSo4NzcU8!CUF`Z|$%YO9-Q{q1m9KaH z65baZoMBswhWO2Grfap;^RyCP4~XZ9&&fOfhk$@g3ukSlp8=1Ci|^Ru)aoywD+_ zLeFG-Ag&rHLd_Of_<)SkgQ~h(KancTiARFP7JZJha=zH>lI}B}>9YFlsO9X*<6OK~ z_}1n0%yNkpgmmm?ZP)E>)3Fd?3DBo!DZM-#kzp%5riLqvb;9=K&Of!l$U&yYr`0T{ z3(Kwo$x$tHh_t*qt?xAMqh{_D(wn*z){ox)Wv7tYUosQQyDvQBUb_VX5vcczoFsxD zUtNrqe$0JZsHhh@mwf*||L&rpJt^q1m6mBBN{_u~m6Im}shLpcb$4qb0@Q3rnU>KP0uo@i6lB-l6`Qx&4TwbLN}99yU<_ z0S@>$(c4v3@5gk8+dG_-ldI={SS#9YieGBpHohUke#kjZi#cciwZmdTc1Af-zj!H# zk0)0}mTf*$JP6?aGeCjKM#a&}-ABOTj$nb0!+-uSfICd-P|gzicSd7{bZA%cV!rL! z1lnedUW@%iNfJgR)8~1DEf;Q(b+xn82Klt9V`0`i8L%^r?{eL}Q14KoH~ID?Z`3bd z;fhGp%TE|?7d6++WIOnaEupwj?a%6demd`l0_HB?kEv(cZi}}7MJHc!KrbS!WYvPdfns zB_MMSVT(!lq)=78E0HeU)FuwK=0^=GTb=gQMf-~N^%I}7IIu_Qz>L*^m&wqsFc|$U zT{=I~`Q~=`nsjG{xI_!D((t=0H=1@LNZ0yqH$3{Z--6BI&gL~BL1_EjpW7ej{3>pn z@^y{#LE@(P14cZhc1r+>2plT3NH#w`aIQsEWB!lY6V$xGp@t>FUU0}9u zbh@imX&B#^wW)brsMAf+Xo0j%IH+{Fd98-f@CewF`3%^FFVxB>HsO3a3tEPx)lmHx z^p>}Nj&3(o*S{Cbq1+VmU=wUH@iv4{JsyX+8b>Ta~3(Nwf5B`I4-XB%i&B2Jm=7 z24C96)+Ih3VO$|IRq6gBEevt9zu92@NV;h`)TOLwjy za~l2FWuUNxSHacsd9H4#?6a38vpC~GqVEddPku>iSkLC*=)?GTAbF&B*#6)x;IKPV zF|T>|2s5;oKDJL1r*Xebxk_7$kLZaCT!fXid_Hl}hezWn{CReRc$;ow2dcuY65mT1 za5A;;x1{oqPfz49tqu#Q#&AB>vE;Q^Bt|I-(mve3N9+FS$%E9wil&(g(p_J+b@S+s z9avkgW{B4fHR0OCp;aaZd(V^%5@8xii}p~ys$1^~{xE>Z&s$rUX9iDzSNG6A!gVs)qa|P{$L$EXRa<#C$fDtt zq#pJE5%rY;RW)7Pa0F>7Nl6uv?rtgR?hsH~Qb0Nky1To(yF*$UB&55$aFHC^B9|=RxvUu6Wz1m#|RfX;^3|*(si2IS;$se^)vY2+!()d>c&-5zz6p@5amAODCTpO8)M z7Ne>w?>6h+a0;v!FU;8{XtEgQy_>mh;%-B0YZNz5UPH@Bpv(tC1v8PrC@&pNNe;9x z4vK9!%J;WZ8z^yK`U5{mgOLjMS1_-ZiG}Iejz?xbn^+S6zP;2rPB{vo0M$&b8$;vQ zi*}G=`Sk|x^H4#bHKW1a-Y}?hus#s?F{cDpy9yz{bJkRTjW|Fl$cGPS*cL}>LPb1S z*pY~vT$$iXoj(bifpkct+u-fm-PFJeaGhwyVe0DV-A9 zoi>te<;w@iP4l;+sAuUi$`YPeMIH^ve0(EDu4x5C;(vhgzbnF++)XADC z)& z|8>*U$3`7(%1A`7+o%qT9C`#vG#MtKHV~Ll{w|w#p}q4e3bqH@c}gn`2*f8^8&s$vpgA3JL!EHe;R!0)uv~bISk;so2vMeN;^h5gc z@}acawerqj+Aw&la-Y1P{UE}_qwf8o5tyw4o7$C5&2F}EU-7~eD;DMn!H(ihxp9Rh z<=^A&C3=CRo+xN1k~^0n)>%+RP2Caa!58Ovbu#)Ri`n98^25O*C6L-0jLoUCI9~S? z7(c|HuM$`Y6-$GNxD?APELKWuB#ifMab{6-E3MKa0)3L6cM?W)tRL$k16g1Z1Q1So(W#saL@epvjyT`09hVhr|I~dZYNiq4YnVrmdg%`dJ0v~)h zgN*hn?l0(doF<2Px_Ab9)>O!05j+^yl7{_(NLUSF->3F3sy!58@AS&%C5X{%| zLJ-E6^K7Tg1mjg%76&afzMY)BS3NdP%ip=eBW)|0b54kWtz$;CvzSm%uCoXg`H2u? zlgZ~mNkP#ICLdN8g=U|d#OJYmN$S&Z8IbSIu$F9#RI|T)B`K!&yJ+QCP{gcd!(cQA zJ>vIEkTp8q?##Nztr$+k7AyjH`o1*?lpI~p=JiKw+yY;?u&Jq?RP{`xEX0%FoRNdk zP4G8`?-wuqTs3O-YYOM$Y0=Ken{DneXr6WKalaCuE!DN4SP@CyQNy zum=>>Y9yLM$xMsjlXmCj$MjD(N1$yDT+ZXTEmTEzztdH+XPx#F^>{@F4%Bh_9X0q* z23U@#TJ9&J7kGeQemH1auKk4w+9Vz7iH&%vQu2mj-r_@u@GFK%4ynq;3CT9@M;%a4 zY8@%mzS8Zm$7cHql9FeydGUp$e_LK^O}2b2WL5mWJwuF;br@Jg{5g?tny5(*OzMx1 zb~ZtKJ7lb|EpGe=$jQl%6Bcg8qG>f=`k>FgG{_2Yx6%8cwHv_@ibroo@LiKaLuAv* zk2)-hcVQ>zuk%jPF(`03-PI>IljWCtRB#IYlGSZXK$Gh*dVqRGn$8RrzeQOv zliAF{rj%_mvGlFI0&9x(otX(WG9`x^KFe|R0k26ws4%4*Ww+%!h!gxzX5+uIz2nNT zhwpxDl;~&)3VO~tt^|e0#=hycp`Gi>n{*3)x!<7~<*J`81m1D=`;ouu)f}Jol zqKsn^H=C+f%R2{kmsKy0?7nEX>b&evqgEIbpaqoig_lx(|pdac_8_9Yn}p&^pvF9iLCf=vt^zvt6vA+nS5Rd4q@__n(`unrJoM!90*~ zo&~hpxJ0&A-))tftXTR5_(l54yG$ebe?Pgnh*`Kj7n^Pom}Y#5yI|aAFEHVh*~c37 z5qJC{oqBe{(bw8gxv@;-TX;{;dyP^(FA-5u9lP|x+Lc*~gjFOo<9_ZqE77w&7HDx= zIleWN#A227k!4=kCzL zYy?#oIUDF^Co9lY@8UU1K@&8!F@m5$k@?GngV0fv>|n;{uU1>>KVz6c|4kF9>`DTM z-n>F3hE3%tjT&1;R^1+BW5+^mWeJaaxOfA7GB9N+o5y^Tc56H@f?%Q`Qua{t>LMm! zb!B?z61Q9t&I#HU5`{$8xZ2g=ZGw#K;*|M$&P{#a#m%3HGu&`(D{cQ#7;qtusVNQg z5{I++QdRvtg9!rLa3qOEOQq zIuNsz+T*jJ`U-n%o8zH(s&eo3STW6_jU9~9?;7z|O4r(g?oGqn%cJVNLS6y)m|K+s z%jDs{zJK#ztPuU{(#y7w8E|P@4yFWLPPQ$$jhf9|)AYj2oR6kH11|K$N}`=aqvj;F zQbF$2`fhfya}pGMPe+IQCd>qR9Ddq>Bhr`x z3K87pEKQWClToSr`_NR!Ka;C=5`%gqoyUwTOt0k?Any|xt=z&@KUN##SM)y_c|$(% z-QJxeZ#Q?7H(|}O0U!NH6MZ5oEOK<54$v*EgZ}8j1P+d>R0V)J&Z=Kd-h6t35uv&IP?R3qoT`f_Z&G#^XKTnTU3k zg}}*%bDRLf1}X$JmN)Hrg0_vAW@3ZR5@$9ov^lY-BAqcE_&<;bu2d6V^WdW^G_8Nc z8GAsfJDb#6tJ|Q()X>?!z$eE)l+>rFQe?7ujD3($H%GPanT$N&bk+FK^iBm}tzI$U z^-X`F4Kz3@q4$9Xv`-+2f0L>W5)#K2{sfze6zUG1hR|DPjaQrgz|^d>8{-!`#d-|R zkjVZHP!F=rXnhHuEXG;9N4YE&Pr%1G8um1q-ltwsp|u~guwNlRO-aWpGT6q&!=nUz z0@xlN=9Da=x<3V)*yEf^rSB{$i| zdu}dEs9iba_FADlHoWe$uzlZz*-&M+oBNHLT!@Vg-QdM5($|St&{=g*u-d``c@V0$ z>w}^sT%|Il@32QRX&s+VvSrFY`WTvr=t(-~5vfjlqN|#yvXh9tUCg6q8+>-15yBr1 z2uI>{1{Kd{-N|(BPg|(M?MH3XuCjsyc!0a`KJ_J>JkyuCjwy-H9i{$>MtvW{^ls8@ zSOO1mm8jomJ*wzB9O&MS0QDWo2+;prDWsDSavrm^WVN-pU$h#V{cgr-ag%|k7V1~C zLwdf~8c3C=Uh}la?nP3kX;w*n8qZaLatBP;Sf$LyUZrLfoW-LkE4o_mA%7U{`_;i= zU&%+qYrv4=$?GF0a90^qvOV8#F_S=z9+X7S<%G&$&7g4(9{Q1fTi(Yv4rtvk61DQK1DHm=b9S zJ7wv~qJl=vD^ce>Sf*SfwAsMw`g?~!Fjs<0r}-N3W3|bNQj>z$l{wCs&sR;heix3P z0jtv4lPzHQ4e(l!wY5zSvX$Cg=sP*i*{RNtGrWMxbp;Z%zR7z&+fRoMD_TNA#3K`~ zxVfi))w>D<&hWD+j44y{<=-e<(HwQHpn}4-fsh3XIXC8zpUQoZt~+QOFs8k^dGkKz zXueGo^{GL#5(^8<8t4X%1k)WSRBG-)lbdOdJ)AoCN{t?wIDF{(jESkE<2SW^9$jYF z>=u2IRHL98cAWC5{1sYA2Zt#q>vW|&F%U6Qj7|Jm`Q(<5rE9|c81=S84{!RL$E8W- zSN~(C72)bP5W9*|F)g|-maf#IOR;&st8sZkybaQ~Z4(_hhh?7@m|pvmx3HZC=@OWm?;IMBLGI7tQRVpqDSknds3d0yeg2 zm_Q#ERg0L|p89I%9)=+5{Py#mKhmI$HfM-5+o74t0)I{%lKsZ>ozvkd8dAPt!8_`= z@-Bbvr+U9GxKVO(MbmYTi?;A0qP+W2M3s=&;Vwmnn>#Cyg{7)7Rt*MQU-a_a-QCF) zd^GiMv%aJ}lhzOJ7=3F)`hQV~6`yO9YRBhKTT5?9x#+*Pa7!?=eTw_XI0a~1#ZH4+ z6pj8+scD#=j#bF^z0w>A$YYbt=yZE6Zx4CjeX=of2iGOOI|^ovGd&*e!=s~(CdiXi zx^D6d*;il+*PSjS--*1F7-Ls&0z)m7n3!*$S~OK(SbN_}@F|sl(IDz_t`HA``t~x5 zX$o#@@lslK4O%7VUm5wN_T7Lx*Z=!$+un%@YEH8n>fA^ z0bj!B)Z~?fPcYKGJJk1d^lpuIGc9M>t6e$@h*@(mSz20R)@y}st#nrTVAL}aw zm~&$3;l?F@2}jo4ioxTUO3&&#t>M(2&zQ>;d@%JVs4LY?pnab`^az)-l#HoTUz~P1 zV)`j?-N{`p)r05mS|q|jSKnAp#LA;B?gykk2fJ)MtKFoc+%+a|Ckk(U@`x%H57cyu zK|k5zPs9}}s5TiRl8Ls1f}U+1u4%qXN4!GBMueZge>e%BbI%VX?A2Wj&SQ<@pZUKA zm77@-)>WBFnzIk<6VUW*+^)~}XslXJf+-$SNd@ApepF@|sk4g<9_1}QQlKdNK{FPC zZq}LOhc%-qy&upC7=A2D1XDalmZ6?iNQp}Q`T#BpXX-!H*O(P@^%o}j)D%t&zFk`* z{us1D9hd-azLf}=u;}+Z}bA%(uPHuNGt?@Hk3>Wp_66S~W@?XDy&V&zeqJv=gK)){ zqyAKOHi^2hrh9JUGnplY2NK4CfGcJozoSkPCneo>fc8nL|IwimywP^15-7N*qrNcO zwhN?i-;lT28nPOS?BvU%*xwL?ijy^=SIgh=57!R9WxJ(I67DI@7~H9%ivO{5?TW)( z97thb^wAHZfS$lSeeH^cw;luv8@LM4=^h0?0+Lwh3+z)u4NL`F3-x`|G)2p{C#)K> zf9~h$@~*p9-i427O(G0Yaw^i89v1K``T{@CtZ>Gvllffw5J+8foyrQM@KHQmQf_S1 z>ZuNx=(MOG#clqzXCgx`ukl1QfPu_Z!6TY~UO1pNdp~J_;P@vbpv;2_nj)4!iR!wZ zv)v~`^F6;e`eN7o(i0E+WRgOnVAB~2W*_pFs(0FXpCFX1^~XqoVba;_GXrt7Gfh}( zlt#zF0NuXbU&87uZ*p0iHxXSMJBEhjjD9Clg@vKH7D{w6JS-Jf9iOHD@K5Psc|=QZ zxR=|L(HtcB`qMu~Kvi$Gt!(U5x0l_E-_Lx&B|#laEZ9s^TO6gWbP{3JFGg=orSWEQ zLGN^ZrOE36_I*rzu79AvxLfz7+`7ZkGBfvOx@kfiH zOxrMJgF)pQM>;{c^!?sQ{jS0_hHJwGNc%O|Z4S59NGdOH_n02u7fwWRM&y7vD%A51 zk&Nl4qDF_#-h8Tjb#d|M(Q5Y>!!h0L)Y4TrMN{a_=RzLCZWk7{{=1Jkl47#DIL^!u zc|Bg2^BElPV`YxZ<5D=;>|Vm&X7gTLxz2JD!XqH`kLM{(EYXbmk0JTge^ti#wL4~H zG$g#qu0b)E^Fy-E5iqJidwDQ{KNrO&?%vToA4g@pb^M`k*A@@#D*|zjX_c8^n(}wJ zuo$xc1{-{rHX6%6LNF|AdasUd+fVb?uV;Ojrlt&-)J)>TNLde7dT+;mCs_w4!tqMO zO#HpeSBs&i%iwjT82U;|NJ#zUDQf7^O1BgkDcfM!oTq7h^EAuD+NuU;XK>(GhnD@@ zud1Ip=lQOob-P+lbH2~Z7OPDqaw7tj@bf>r3Ldjj5=fO}+6qJS?p3Wf!~T0krZk4_ z%OW)SvPn=E3K(YTRx14g;I*xRa`C6>^-n@s>QHeU!Vt_lDU8U;jKMW7ch>xooBI|T zuDjms)x`6hB^rA=6|4PVkW)O;I5VsuL{G!BC61rmu?A&$Xw|;Jf6}za=giZaW!D&9 zc1N0!4LS$()~hw1ZM6tQ%Juez1jW1{!NY9XCmF(QumP1VLCEAwPyfy%K=_(EBIdY= zj#{c3sqADrRg6QXW^YbKGr_Zc>34ga{0o}Ar+%+z4#rg}-Yq;E9QRaji;!_}1cVxJ zy1t;w#1v;_n7@)E&Z~NTR=VL+kF&i>*pAoL1#njpg?~aD&g&R(5!<8xbJX51x*JU-|Q@pG^ziO~J zBJfslBpdZK3D{aCla|(Nz+LU{>BZr}$8b5_B&VXHDz{k?!^UP%sKTwc*9wbt^rYm7 zA93^*30=tl;cUC8vUT$lPd5|{>DE*0INoV1tK5IKY@90TM|q5_G#(?E{TCzsh1*g2 zQ$uy;Y-w!&3_c8!b|6f9tZ~si`5CL6UcwtpJf4BkS$#nwS5tG1hPzY%u2rY%xx7fR zai0C2e{$Rc*Mwui*cUAryk!jZH8RruW_f2f>qD(OBy23UvP6u$9F0X5XQ0#{O}7S` zV=*Idt0iaCEq}?mv#PCJZzUTJNz(`5iT@=VWM=;m(M0wr$k{i#?ihU(*tLd4*!|fN- z+3G!bvcYu}W~o>r@2; zuE9qmvVyoP$@L~T8TrY}WB8e(iNgYVwXaqb)`@~W?EgS~m)CI|_o+x`TEp#e{~DMl zkEk#na()_Z?A#>XAiCeNlDyTtsyk@J%UU(H)avzQ?P?>1AubcC<|X?%?JufXTLRKUfamLc(3eO$`xsw zfqQQzD1sR^cr$7uX!lCZK@_rqT^az3i4&W^Zb7Y-EzRZOYjlr#am0vILT&M8h1~8+ zvMUlNdTG-CB^5M_g;O-bpMuAL9rL-|yL=qbTYxShL71h!67yQyBErc6-m96+wd1Ds zA}D1T*aYyrFnSo?Msk^dMMkwqs*3ghOW0m+Dp)yNXv+&2c%B&4H-~Ock*LHLy=b4y zQ=;tc>kDTypNz#k>9RA+;dzr!)bIy(f8O%V%9083T*@4u;McPYFjd+|R};&QvjX4o zqv2jD#Uxocda2P9jRV{PXH4tKSKU_@bg_)?+x|tzljBjplQ@APe8iz5xWm9whi)Mu z5}hARY9&S`yuS2L4BC^GB_^~(ps<`wUV7rm_bwLUS-qrcu{JrNPdq_=dvW1UYSDNX zw|HMHwLc>LtAjzg%bTKLYqkg^&6%B!Kjo-quVvm}fCr%#!#XA^462UbD1>L)8-duPW~ zopx?Ib{Ll_x(DE5Pf*?Z^}=)UySmc95eH7lj|Wx-H4 zzElL4a@XdA&+g-795@h3a(Tz~!hsOEaH-Q4%Ob)k+sreEHqRamqn*JQY}F)Q6SL=1 z!bPj|+%w!ijfQfFK#B&iXkd|&dfd}jxCg6r@gG&%8ZnXtPY6|?oeI*7c|c9!cXWYr zX~>c>e*!}f2lR6VKwJ*7p;-25_xr0Xhawm$^0*3miC;4MUeBdu2%`Yjk-;Wl_{vF^ z89vtScSp4+P19mCH0s#n=$?It8xYq6*|6<4qBnyX?SnbmTx^7_CZW)(VKAsK{$_D? z#&1ODOTHPN;zCc@AuBndT34jMKkjEhx$jX=8`P{2#YLule*k|WFvcM7AKVI^g}E;qdreuAYD*k@T1e)WceyS+B~kET ze89iBLS204MOwg7Z~^ik6kj#=o4hQv8UIGUhC>WR_#yC`9I0_U=Gali%*%jc|$rt)?phW4|A?l?8M)i48XiL*kBAE(Zf~2Tkv1rsKG_CX;X| zB60PFQq@u$ZXj+C2M42*uezFsH1=RA``y5_JQe33W^9b+{F<`qS04qGi4)VlQvS{J z00?~=@ZmPx`w%y>UCLgV#ceM{y_oivnVc6VgskK0;J ziJ}UhkqEwPLn;@dAPfX!lTu#yuOMk|FVdIsb5eyR@;lwC{Tk5oW#`w|-e~h6k#GLF z%2FJI3l0wD5a#8>ZER$O7!~dfS&L89$p;ZJu~EPx=aL+L%TTiJCg!$Ptx+HM-6yeq zUE+-5m}6umL_n5{t1cR!_qHwKz@V)1-}UjD;3Nj9C6AJN68{7EMdJHeeFG*Dj1&6@phxi;6Q1eJjIi`z-@!u56Xxwjh5$#@N0vg)S;SC$yMh3s$3 zup%};jfsoZ%SN%D_#7P2ciI}-e8qr#gMk~stvw2N?Kf0&U&^?p~1Z}X$ZsFiV#a@P47s(MMTb$Hw zoVq`zB{R@|)?~CW&S!34$=P8b;zfip@%s9@v*J(kjpO6$629xFM=ftLJYnF-4*g&l zf-a|H)YOfJG=t+WxLgm9*v2RR2K6I?J%9_tploIFb$n`|?D#gQUm6Q9r2+QHb~o;$ z&sYObh?16gPQ%0^^!mL7zi9c)Je2;RPhj_VoP{Zf0^`eO78Y3`;VFsE_a0uosF{5S zGgpHJJ*MFI>gEW!+|F*C{iZ3OSmHysnm~5njM%IAcQ2s3qJ4E(lz^+KZk=klyv9L; zerHcNU#Y(CPc9GLQZrSnOYGh4!p;_Yi6!~GS%+3%PbKH|UpF`SFNOx z9xG)vxfvBwDNpT+&v+KDo*5b%Zq50>nedRANbg5ysW2kBm#}}I*{EVfOhQ$h>63ER zTHtPDe%yjg?F9qps;O5!qqJ76!a`@*YSmdy#BGrmi%I(^E0@JE2ZJuz`MP@qv7(LL zr5w-aUpcuNmB)Lj3-Z4A2RkrfXqAZlJ;65e&2M@}t@uljmzNhS81!%kEE@Kfy(`;J z9T$AtJGtF#X9cZ+ZNk4HC%)HM7YW|R#QevIK5rTh87dJ-+{VrYl6T)@5<^#AfrC7` z-HN%;mL0)N`^C1cFST=lGh+2&W>ue#lNj=Nn|g(U`ldL0G}Jj#Z-Oev*~x<=N0(oj zDvLpml8^t+vsR5NuNz$Q`;n|u4;m~CcV~#N3cYAl4(N`~a zJ)*SpL)`E1QK4WUPf&I_HQ#-GxHdF1v3zj}gTl{a^$yo<=hsi%Kq7MCR&Gd0OI6hT zO!ieLr!Wk3M4WA9CQzR>roMX?F1y~&8yn#T-%04nmj_|DpPZNpX+DS7X9E1!l*f4d zXUQI1C%Ujjdw+(fhF`!13Bf?p;SFAOY;I6XNQTC{nfz>L>1nyoso5N#CZzchfd8me zU{A=0Se-!#veQOnnx-vFuskOzl-;Oum4ksZD%QV7^c~S47>9y-5q_*i|9(OZGoAQJE4(=M#aw7yD96a`$0=PRu@t zt~kY)1$qjOWvvel&d+{onPi-JG}k0JtjPG`Ere|nLa%fIb5R7b=Khq6IXYHwfyU|=)g3AJXnnsmBNf5qxQ&ryBiC>)n}ZF~Hg4kzkklqZL_+^JA9 zjIr*#>)~^{6sGuh-#>+PolXP<2WR{HBhNIuxh&*l@dYcvS}1>@tU?~>$#Y12Ygnx6 zWv(%#)^e?aj26|>c3+abAjtbJ1ZW44o)kpu=xzjonjYq9l7{+*Ed&pbn9C4~=Z`kW z(~|>foRxJai|t+edEoAi^tdNeW^j54W384>fdPKm#a-Y7VK>-n3nU4EqIhl#ru zl4{`-keA=woAt$p5sqvzu~mc3Q+;au-4~Vimj#h&!aj-;5`J_y<%_Z6xqsXQF0=w{UFOXYc7x zzCWI1|0>t*^r+l0uu_PX)nA9F?k}GfEvS9+b3{N1zfis(Y*Le#+#POv*0lqkkpN+E zxAWAUcN`w9806aXLLdoEcH+|REgYBZ&TSJe^Hbcf+aM{TwJ+F6N`kk`tW~VJR$zik z0S)q8xt+h^xytfdpMdK&AFW#Ta*%iIqu#U{!pe9_9V3o)H|EtL4KheHv2R_=xeBaKMHD?Ugl^%LgN@>-L33b zk!r;HvtrP>xWtIF_A=zvMA|o(gC;c}&8ni~E;NMP%6#2fNd=%MFt7nAb7cnHN6as2 zBGb#woDKG#(fsiLpV^2#fD|ol=oJF0HyCVBCo!$~TgV3A+5V%1=SHPq_=G<+fs4ZgJ2EUS4q5RN4=JH>($T z2>72ue0nW5hTdHJ2a6#3N@k2>oHBQ9V)6`HnpWn%KS3D7BrKiN}lYuYsR z6dT}e>>VRJ;{*(Y2Y7teJ#OyqckcP7CX*RAx!6!CG=$%8aiE94d~vkX|0P|GojCJO zbS+u!2jSx`U?8}-i?E#^no8Tw?|-%b-9)3FGW0!$@q`woSmIkg!&^6seM!2oZycfsDZ2Hc@Am38~U28 zZhj-Edoxp2bR_lRI=mXk?87XzT*3I4!%%UKk`q=zzTXdk4XaSRDt=;md`9-c)vCtm zEbS%qH0H@l89m@+h{6o~ia&GN#ZT^WTl%SEzWyg2t?yw;1aDhB+!k@3_=cI0b}#&O zffx1ui7(+>O$L0)#-Q1d`?}!E4ewQEFyy}6NcgSa7AHZRK;V>!S3B=pK!kU9oHgL< z&iZMuzzb{$OvN(>tTqm?^%4}Q^US06yAoI`vvvJR&gR}8Nl`5unPmE`RmVG-!YmKnHzm(sUixZ~n@sLkj%=NtO`Nb5pyL3K01xH%`e%?CPil2Q%JcKT zfTUH#-QD?FnbX`-KX0B;N%E2Rx5N=qmLSDjil6Gd4 zSYQ%p$;pGPBP-ygPqw1Cr?EW@jiL3C8VgT;`_se8phVd8E7~&fHV1Ik`9HE6zUhi3 z_Ey?^SnmoNpPtwoFXKc6hwymNFWu2kJ96eTE*kYp7Omz>JpmTB8vcZVv%_$-A0y{h zQ78}|oKdNBJ~P{+9&66#%9pHRLD*qCIefEsv*i@jK9xFlr{rP|J3j?Q(4Rri~@93EYuk~mZ_D|xyl4#nOPbx|SzF9Ylc?zU@ z+y+^s-+Td?3Mt)sqQXOZ8$9`tHer%p?QKgz=cMj9t8D4GbhWei@tcsZ4yKCeP@kG? zkq%N|6hblYtL?n>nySUcsp6Colk`9HRtSTRdVBF{dgR8M0#D@M%5{#>Ha^CmJ`JC4jRlzxpGD7+6pVd5Jfy~Hsio-uaM_NhZOnEU z$oJC_pxbCG?#OfK0@IMhs>5@`$yobKu%TBH{=2hCBS1uejKRZc&hl)UhcpWz$f>B4 zY2J_@JJ6sn)JYEt5*j>^H3DN(OWW!fovdEbv33&3D|9)!2EVVl$>ivm)X=tRPh#8{ z#_xDpa&fUa;Wmx}g|l#o+90ub-ci`{$C5(x%$)hI0h=Q{WY~9)3sS(vYt3#oFEqVz zb_in$W$8xE;89b5wUoee*T#eq`ts0gX1@p^lO$(~-^y`+*V?oF7q5)%{daDDRD@KK z@CGWPl~`BXWCUwfnqVHCD&(zw9(029vUo6_GfFpyj*Ck^>fUI z`!CFefy^MXvet~cwRj93-?S~1*>cmxv%Y)rd>p*yA2`R5apTE7oPw}~1St&-4aJp} zt>KC}B-x!dVO@d21V(@AnmOBmT*qsKleufRp5N1?@C(8)5PrH+gLMbfq+JerT%@v&?=kMKlU1H)--`lq zti)VY5iqYI9m{4WHq&TyEPGaErn>g|tJx?}QxC#oTS`Q}p*k>o`<7X8KUu@df-m2l zpn6-vvmR=G)Ft(fR*9rOsq5C79PGz3O5AoUrjUqz{;i5nRobwK>UQDx z)%SwC!Yq!190j`K5e8WCRacY&Hq8-Sb={W@e>)|iqulKN{>Dlo-sd&!oYa6%icagz z**DCE-P@o4&MpOUK*d9SBfC3b!x1DVq8B=Kc$p#p+5A(?OY0$|>#YjXbEjX<=vE(? zf{a35ST?m{GQs^oZmc`=9q#ePrPqGT&W;n?FQN3f&ZoEw?&rKJk&FO6AEclTFQafP zGC{P{m-UvCzot*oXa0NoWo2u>*~BCWq%1X_%uiiW)-YsLI@+L|)0+FYx@qbCEnca1 z>!F|?NZ3%-#mcB>9-Hv4B;-xZCH!qyWz!VWa(}&ZX4$@XoiIE8G=(QLZOlS25o9wQ zlUgfB2v}`I{4d?_R{s7~9vi0RmxcQ;&v7W`CD2kAr8{HYTCNj-ijw`%^pODm>7`aR zIwBE75supY#a63CoJ%P9wuR2|r)r5}UE!jq=v{^y>lFRrEOXXIsYY#iT{=x+*1S+5 zkb`fo47}Kn(xKiV+={bz`GnxN`JV%&OMh75QId0Z<4-%=e-tNiTD2oGBw1ca2f5($ zLcd7HnBE#5mXBxcU1Q?TxNg6p?5rD&RsHTmqhmVm`>~{9pSw4s@pQ(hF3u~kmfz$F zuwjB9yqG$$OlLMvo8|mmZ>MUimCRjydj4QM+CE^A)ZjR6`Ys;si?c35THN(X=#t`h zXkt+Bet23!b{t5iiY(=5k_9}Pb5u6%nYSk~Jt6J2Ao z+f@FUW|7VW#gA}!{+3VWtB0S4u6=F&*EYY;8PLeQSUBjg~;*o44n4FFK!qtRJ=tMC}pT(zS2@2sMuw99mzIS>WFv zAFuUy(QB+zo#XSe-bM?8(8E*D>jkcCCX5H2JMq?3Z+IW~?e1g<9vf5@!Di-g7YW4C z`S0279qb!)eBvmL-gFIfSgGq7A)35^MB`mv9-4-^`341L6&BKlmF1R^}i4YwE^0zfdRwXye_ zsv*jzoZPLX?eOpb%rmdWwAkI@p24!fJDR5fd^PgctJwozb(wJ+T*Y%_Uc@*4cxZPR zdcpHt1 z`1{jqTxYA8xq*V=Nlit>x_}9tF0qvNBA*$Iw#bT60 zKv&}m?zmJ_E5gE4WN7!kqx7rzmIi$VPZ($dE)tiNB0jye5_rtj>j@YmrHIe|6VdOj z@Mjte=qZ}w@W%OexjGNj;@MpzcT>M7TF5J5U>Tl=a-UcOO2+W2q~Drby^G{6D+v@b zOZf%G;kb%cya)TVn{I*NAVIoNXK|e(q6?e1%m~4V*uO>ce`;`;Cur`1@b}_oD!2T1 z{LMg#Zm4*`o1n`|VMjPvL{19p%~g%%wf<2_1A~_olmRn)B@83A%?q5)ySy?`P9t&z$aMQYSGD)9x}RqC`(F5n0U=Ms<4iRom88uO3kt`_?T##H z{+5XJJEEo=?3Dw9++EmJvH%uIW@VN9(xG_leJ3DYO9v@;9dBuNGq#=QDfNBL&J zkH!D&=y$}zX@|)B=E-=1Sjr&jtl4P4c^V50w?r`_W&hc zUPvDW@xV3x2TmrGhkss>j{SN}fBXmBe|? z;lRrz&HCN_#kAVHXHJWZfM@`9Q)qTB{pm9b%Q-HSMNX)Edj5hm-Wr|V@&=htRM*e^ z!gY4**SDY%wSdYL^fhB&4*zxYYCfs?nw#7BEApJrl(lssgrxZ+!Ox$ZRr$ja+14Qa z@GiBsJlOF#x{}#w0N<4SET}t=LVy1miSB3(1yTqFgT_6;Ya|h0Sm=^lkT1WC70p~T zw`@3(7XdW+vatP2=z=8|i6Mh#j7wS`?oz9%?RxE|M+zXG;{A#M55d5f_7tFf%LzV4 ztMA=H0=_y0h@QP`c0PUI`c1U4zqpk3TiJ4z3DoFV%DHl8w|BR*i+lo-_(>3;l6Yw3 zHG%SJh#QQOW^7CyT&zoZHb1)GKz9@u$EP)~Q*!1jK0%@i)&ny{BNAt#2PjMExY4|t zA>HuLNNiod8Iu~FomE`?NdjR8eUe_uJC@|3o*uHi7xMdDx?-RfVpN(c1C_#bxXXx> zxR2g`{&^mZC7*fU^YXxg?f*N`1MEc{eGfPD1`3|@rUX1C1L+?WAGHjyRR6~XAU=%2 zsF+%+JU<}3-?A9kFWV&a#rV*=dAlYz@kkLn>k#dDNtaf`2;nhUD!^OE1nyeZq4d$l z$ug+vJj$ow6R(!WronI|<(D1bwr)OXOAD{wQ)RVH2;pEHYA-5_CT&D*%0oF8 zfZM5dfr1CDQD2{?@6L^JCRjs8ozx!`{t*L^5W=StGt(zvEdtWLVnlU zv}g3JxW&Q5cEVy`pQ0N#VEiEWq1Ub*4t3DB$OJZ0vS3?>53T#r-Q@Oy$F87y^HSNa zCE3j*Wb+b(=-<0RgNVawMReYC+$wuJGo>`IsyUB6qQ3Wm?4_ct@UZ64^ld@W%1kGG zQc@BfeKtR=;kgsTvHGAjt0e6efI_Bm~s&d^NM zgs6zQRYoqTc>m?6f&UT!@86fdMJeZ@S4Z*`;ps5CmCj3N8vOpWmwF}GoEr32=S!vf zvKDkElU4Y3pT;KII(qXd+l_jyEs)KF!E+J znH|!bTuK#t)JLcWSU}9HiL+u<%2^AdfKk1^U6%NFii}FTlWES1ioR!9nk?bdwgQ-FZ|EJfq zhdp0NN5YmRBQmU;u3U;^MW^$=TPm$PcX2*}=#p8lCpPY_0G8Uf;5&W!mVJ6&b=;0` z={5Yv@1A|dO-u{^UEFqi#0_1g1b1<_4}{g2f4f2EM7+_7`~O0{X3ExsX%c7GpdP+a zA{{hYvF`pDCqW2MhFbO1!uq9U${((joTh!L8S%yUnQ;`IbGTxvs<=nVK3FK{Jdutn z7}!AaDq)P_rGtsh>?&kyra)54%93yS^oa96KMw{LsMPBif%^^adcCqCtZslsvoUfP z%_!eUgj$8YY}3B`P*HuCJv=GhuuLi5T3kZJ5i6ff+q7ODh+BU1?)?0CMYyXeM;Z8L@f#{)CwSj?1zB58LI$;6fNu!y`%dbf7uZ^ z<0EJR!w`sb*+G-AwU~$e@x@swn{M^(9RodXQ%j|fPUjYewMJ@wv&1U9?KcE;HknxF zi8Y$+6C~iYPS(L+AuVVl2kH(<%q?xxwPXE7EP;obb zQG9T}wdmjT;N)lVvuu35phw4w09yxXOmQ;Q$*6?^-<{~}I!*C3m}n6S{`LjD600~4 z!JQ@uf`d{*jlmsM)!do*DRnQAK_5F$3k|$S{<_@18sL8J=Jh`fZH4nXjz2j)WFd_m zE;p8OU+dEVZqS+r1?LNh?rY#D(-w1lGbVYap#H3e%Ff|1G$&X(^!3mUvE(3a6&?#Z z#b0V)ASR4LWGr0Xj@VXtKLz~eY9BzeGX~!OY``1j!dd%7XsMwSQQL}}lhL#J=BJb? zB06j;*jcAkOcqWXl2UBnQssMN1oST|qUrp8G!<^kq zT0#W=KSx&=_TFV_7StE$EQq7GSDzGYD1H7En`IzYiZLuKxV)&ZY*#61xw^}kNJ9N= zC-BQ@euaXZy|N!ciA=&9Gn8Mt-+(jxKM7_)mhP3wS^bk{(9kSIr&f%4DUqqp2nVfl zKw(e;R<`gw0FPZg{~}Dk?R4-3t~#|5911KSTt49{oz}V96CYt8f47w9dz;*xJdSV8 zGU;h(0t`NK5d=1t7r?Nfuj$~30M8Lb`v%^-B2*<7{GVPU%=`~^TH7Hd7Th8Sd3+c} z%F)ujX`&Hg>QtXPkzohnU++8hBt>#ZXT3XdTqmcL!mRon7m@Xz)_6kKC#ot4OITPM zYMxLr5s{EHrQjaQ`ur5DNvPO6AwkfsL-7;12M-qmKAi_=g~}xLrkEhX{O{>9@V_~# zOf^$fqq@4dh%(0V3BGRoS=)uPu%J7w6Nm*!l*BQ#a(0{{lLo_QGD?>#EmBV2TkQ76BeMgh-nd z|Eju#*W~gVmsSJU#R~)fpD+uSwpMp^ogScHAO{~-r0K9&L4dR!P2(M+!nq2jrfIAX zbi~Hu=UtN=IK@&H8H2XJFx+sufr1DtOscL2x*ZS6QwQN^a z#ciEs!o95K~IEoM&)!=un$dNx_-Z!6v)CE&B2ErvyqKW%6(`!jn6mKa1pYT15QAenxzh_J@aL;aRcv zQmG)1F6aqnDBjAvh@vM8_YzibxWhmC48Fkff5Mj6%Cd7l4AQ}J1x&lHz$h;U1)!Rv zqX`N63x1Sph?I9;!RNL!IrylLPV|%=EOYQR*FiE=34EK7k}{S*jEJgu_k@__(fy?J z>QWL5Z&}c5Q5;}#a@kQhAr$Zb9~wJG!BFboj0j9BC>1n%A-N2XiRFx?OT5}`3fv%H zjDi$FYVZ+}kiyLNMg}^NMxCQ#DCs={f;N3Ms~<~It!nVm(qRps>T(;s4`D+}=HoUw z{u3-xFA$Jj-_azTjXl)0NknrXuP0_Ne(xz8FB!wB|m8L4{jQbkibb}o9u;hIN}p-TYpP2Rn1@0J2T{9yV3jZlRk;K zu@NA7JLjT+o>Ghfydn-kUZ|d@X0$H%a;S=u0~y;q6z2@5iut59l{j;@hi+jk#VsXs|>oPQRV@Fh6LFOSl(1TIF2X!xgY z@E;}hGQKt4weDu)&}bbPo@NJ$;Sc}2byTdy78VjvS^KVe(nuhid<-n4qb}zEqv|We zs@lGLG4rMtVkrR&fL0`EHB>;3&-zw)@x-fPV@ zW6Uw;GKa$Sv`4Z}u-`jRMJVqJCwqP1U4}hzbk(l$R)ye!inW zb?anfXDoUmjcAUMs{VR_N}{gVM)Ftw{v`EQfv~w>`;}ilFFuWnDrmh#z$0^L}77@*iGITJxh>=e}toP{rmDP zRFE^186BqE)7r@c30lSS&SGCm5iC=)sM_uKz}%5U#_uL6-B|gb2$LEe({iO=otqb$G ztk3&LdB=$0zv1od?eT&dZ~pYEHfeM=|IFce?@^WXD_~HutYJ^+%&85JFPK=mA~*HX zpyu>h=H1b?912F&?QNGD>N2UzK-)x4w^#RHHxHcOC!T@bo(J7mWGSbYS|mF%kQJM^ zIwDqSaS&2yd0VNoyqXR%8t}ekYXo05cB#O*;gav;-}B7bwENJD7d#0DHu!1ZP(`E1 zTWpZO{=;sAu3M`#D1JWf_Ek50vl!2;aYjuPz~vMvfTNcXEf6I{#MEozX2T>u74Emt z&>1_s23Hu16goA0(*O5l&p>Hcfm|1_Sv4zIF8V3|P3F9=P=(7Kk?>u42)Zo;+R@>%$+y)Sf<%eX$B3I2qW%Slp3)4#GuKLmP0zucLL!(9&1 zw@?I=E*3N><~_0`4NLDjQY1p^YWD;rW>SYP0Qz)j4UU5zROj5*-;cEngf81s#Rm>}XbG^r|E;(w zS0h^QGi+ULI$*-~I8J{GsmtZ|mSNE(N$OyY>WLcE7+J~2bIM&6?uf$yNpf^@@_IXQ zH>r7c{!Vv58vY+Vc`MJ5QDs0#DBMVEuUZT=oq*eT1oIZ>dTAgjzo?)MM=unh=_(~`WFc&09m$wMbu@%WgA`})$UBo_IVw^bE1CifO$3!rTbN<~UO5%0(k{{5(O zQfdpc;%=dFLEq5D{(Em7*y2O7^9hp|RdTsjO0sbi>zORj1Kj!!_TJ+ZF|xkCC?ce( zrMRiBWAGj+@s|iDxeN=D@V})@T5W5iFz^d6RJ3A+8h##HW?b=z-TjRMy?C>>JKV+9 zy#>e$iMGd6l}6&!OyFD9VBCOZUkXPHIcu8ibxCYM|HB|wcrv&F3sd=Dmk3T2t@m5u ztB85#7I_tE#}8V<0{#FRnsX;(2TrM(@m*kB$6AMfaHAmoK!lmrPhT;RHURV;T9r^S zYt`O}?cg_{{6th0c^$#pJvEH{-ukwE z@dN-1R09=9PLH1avu)rj$v|6*BEu(_X;EMbVs~1hYWlvp@oLpiB25hWI#txai4FP# zmdEcZQ%8mg2yyT=rpJ6yjRT>9{1NOH@*sY)(XS3LGh9rp@b8L>zQgi9Izm0~ZJEJ+ z_~?5YQ)sN}^&r}TpdLW}Onem=Ch)Opk*g)*jTt2fef<3KDihmf39w{iE4Bw=3|h&%bRae{a4>&g)>=84+($%xS*WBriZ6ttbztoxAs2_FHQN@ z6{|EVc00E<`MKDMq3p&V7wVT;8;lc&vr^#1Eh6OljT2 z|Fcib40pWE_w+v~RL{9}->&}8`ihy>R$Y85&tE|{^13B*76(>+1lO7w-3E$Y(_c+Jlb&) z5$^-SnL{AmznwGpf`~#vr?iNT!|s4%{VOBRKo63lEZU#`z`vH$0DBwUzk7H%EuCuH zRa71wL1bCW?hFH~fxId3F}&0wXh zWhdkK1pRNuf>rjV6>w!GbohyRmfcmg*|KKF z{K05Ict~+_5+{%gbD(S`z3KMx2ndY&OdOVK^}VTY__!y=B4r}VsTlsAQOZ^6w3l~e zXWM+W0)mo*9|v+nG6sJVJv*VqLguIOG8!0W(6j$G;YJ=be;nQr9vNw?tVsLWZcDf6 zD+d-F^c^MOD^xcaw02(3kzgiQb$W>T=`wKt2YzhAr#@Iy;K#fP4ofnjxSuIVSl^KJ z!rEcRB5x1=XcQBQ(XfCbgFN}k8g5898mo-e@8?fL6m)caTt!977>5uM%oBF#uapwT zKhs`bt`ylx!c5(B3HTF-{`eWMO?bhE`rU|aK27Q)S^5y#dnIB7Sas~xuP?@BBqV&Q z!f(1i@FmGUv`HxMkB?7C`A(o}D+jlA{1f59ISLx38%S<6Ux>gdy?TZ8Uugg2P4-GQ zKtolUUHBG~%53r)6ZZrt4=7n^%dX0!kwQW;dZOwN1TH9qZ$0W=K*Tl6ROy%7-5iWDqj@&yrqSoP!Mgsr~VrSUmb(_3H!o)ow zGwnXh&(G-QbbCBJ6-dr_6oUIF(*;3Z9^&DzgOPe!<;G9(_kbb?U^^d#6i*s@zrjCy zYN)JV)7Y6~z(xy;O@oE=)o6fC_nWap(ZoQk)E&IEoSc37_iobp(Z!d=hmN?TM7Z$U zP$%^++-s6bOD?M+lK8x1HjqzPy1@jg8pEJpVMYO6I^|C)1rujnpnciyP#sA5qg5gs? zo6x_R4phi{H<2n}Oe8-Xldw7&{0Ygv3l1jLVdr?pvvW6kr>P9Zky`^Y0gziv7!1(iP%BP4q9!@Ge7C=N0|k zuOZ8)FIPWLJU9p#QwWy&(u5EMpvBc$-Tpem=-K&Zytk0LWXSkgY303U>Q-*eix^cE z70Kd^nl^}vz(4+gl0#}Iw{K~Q&@?I0Kqm`4_v6Qp+4^YynThU5m{K8= zzW)BInws(alk;cJ9ZW|VlmOBbwS%Hnv+hCNxCkdKg3?V6CAD^%^4U+RE zgPvbB`P20?G4EaoRzy=tem*vF97_ML(Le?wCY`bN7V|fw0hMFBPEh-pGIDV8q_#_Z zsVY*btal{sSWP#M&-;&CRu0?ZX2x~GBDuR6948+^P7Qr(y0{5B3c0X_t$osYiVM-k zj%eoFsCcskbaN6ijP55xUoW9&WrsSPPkeIwb(8zS-cgQ7&wy)758U6gNx51Ki(oQ5 zmPppq>2y5)nud|)nfB*oK`tL1e2df63 z8En0OVD>!*UvGuiOOf!jLI+7vT?v#!|06X3V;R+6EuVS*l-xR8*SR4nDNZhyl-dBg zBpfaSu^&H5(u)^3f%hj9#*5*g-JUd(Ay_fPQ8nag|+avpx~&*^q7tpJkQpl?LCWI>HA$S8kLC>g^2<+S5TB_h}9TeoG$~INBI_|MLR#ryFbXH+(DSi=8<-a-8)zuM>I81x{-z|9}7&OUfn zE$efSf*%kR4$4}G>W-K;V_88FunZxIy$SvyfIxBk>E$grxd8MptO+}x6IcC^Rk8xls`+Y^@2}yJqW(TSB%55EUc07@dK&`j z5N4Aw!0O~a-iMX&3)dE>UqR0GhE!j7XXkm?)cG+<*>?Kw%p({Oe{WEJnheOr z+GZ3ba@^{yd<>iad+Nj4A(KK4dHL0P_Gx|P5@zr3k;k4<^B)t0|5%|4Moo2@&2G0d zAL(#5s|^K3X2?*5r?hN?UjPy;R#~d(y)+QV-6}+DVd7%{ue1+AxbB$Tejfee7UCy` z%X1`9;Nx`$`waV(lhyA0%a!m?#t8G`xVv$ut@OK0IcGX1rv9($WPE{I?4j-uCJ=Wl zp)QJ>mrL1@{*qcNsCWO%fA+_r@5RL~yQ1%<6$%zKD!el^7xqOR3azE^@z-~1NtgIr zPW+%BEsAA*an<(xN`V(rwEygORMe7tN`sP$sqwhhfP7|hG9WPfEhPpQ>sh%7$eVM1 zy-s%M)!dHY@Fj+}71;56Ng`M(Oh8JcroCzXpdVpF^S>l6Ev~ha;sC$pYHgfR0FQ(? zhY)7Z*Gtcvw)!}yYJ6@=zPcI%_em1*XBD=4pBu*-{G^yfP;Hsv+1ppC@nBb6kz1L?-TBn< zDgA-^Rx=(wD96Q+Zmer{lQ!2XowI9yz#9Eui3Jp!6Qgo~{5XHCrL|%^y|6j)st*Na z%(+Qayef~1Ey)RpT@0`G+P>^-)v$m~71mw$HSQ%C6ed0JXopHtq!3=_bO-)ci?0pl z*B9KmZz6c}YWDB4h_-Th=Pu7l>Dk7K&kJ7F$34qZS>tE)v z_iZNDZS;*O8dZvVdxgRVj%BF0&bszTQ3nMFI~-KMdLx9dIkK9bm+yfS$68zMUSDESs7q0w3Q~gAM$f>Q=NS>4fEpWOlKI(cUlEZ4Qn-Wlndo zB9IPfUri65naMU9fMHWF*2O9riuF@|3Ab=cQ29wEI`n|AG(=r2Xb@a;bQl1C zktu{(Y`W2W{D&FxKg^eJr$?#qqk%+6{Pa!{bYWt);b9)P|6BF1AY#2OeZnrj#7iJPJPARi&^# z{_{y0FaLh>=U1z5kB{+}=A?ej;Sm#11NNeci1#^s0vBd*aEdmu1tPyy_}=8pGZEkb zEjbx%M3eF*NbFENN>K(jAAG8-?s$Y!|T0-U?P3$Uk=pi zRi>xNx~#~iOyN%m^Ud6~-#i5!)Db~@^DsDIJlcActosp-CSPOkB=x|9kC8hI>n$z$os<1(!g9z^GD|HNNgUmd8RQxdlXQJ`{NDkhLzuRB@v=IE z)!@T-Zq67$@e2yU^wv_Oq)}e3*v!bF?hDTzMx5bt4lo+<33$J?Mk(q{3^ppcRr&!E{oyAZjtRBK0wMak)VA%eadiF`?=E- zt~dH=!EAre&?A^L1R17%)9q)uwLDzHs}|KbPx(aJAdX{cSC0*8`xlPGV-%D5sfZWA zVY)A%&+sz=R{GNowH$bgWl9doyGaTv>p9Nj0o94o1N6?FKak8H8^fZ4^ym0cua^;a z1|>)*8;l^12(gEgvs<-Dgz8_fz7MQy7v2a5)f_&^wBlVD_MVDy01J2p+xJX@QJA(j zQ}CR>F0bfin7K0jf4dLS`a|6Tv2~y&?99NVg@VLp z3n@dLi%n_Dw+6H;%D1BCIh(jfQ>Q0bRR2AxEc_9@g3HH!<#KGh$Y930U4spYchgwX zis5``W!c%jGO{W9#1`E4N#>)wt7S9kV{u4_Q16IENy(!nqbz`kiLgkH45bDBIh^dc z&So7jV}YuGIdH;hW8Y)yjpps+Pp_Dob@ZtmcPwa%XV3Oh@MQIUS|t*RgRY55uCjbO z8FZzdgA$Wf->?M;rT%{$B?bX7&yO|~6gfE?3sIjMf7q>9+gqNXG|^H3a*+~=b}m*5 zI|MW?%#_K~E2NkxwLiYY%WG0@z_+)`!n*;9>OWXO3W`zVkT~iZ5^Wmg1_0ykBajXZ z$!dLMvn!lMFDtOy{{~-x`l-!Uddri(KQVC?9rkx$GU&>f#~9V6*G-WGj3NE?9+Z>{ zP&L7x73=broUvB?3vuIj0WyuEt{IJY7{noQtf*{KC7sRW5T(6rY>K!}@RZ`RGNp3gi`TURf;&t`7Xi1r}MimH%qLWf7d7r~S)u0p++qz8x z7oYb^(bzKblVeLb;`GGkSyYsH$fg8hdNd!d%Sl9K5TqK`FQw!{ zhR8TM*L&5kTBrc0t|Q8z(;@I7ylYUxChkvK^vC8_ePKU3k>V~C9!&E0OGnqizhOxX zLn0u+k$nA3qKlFA1)GznvhSu!1zz`NTgN94%6w_kl+A;zKd4lrnCQA+JAUC z?B7BKt{)H{tCFs+(t*T^6Bg#)BN~U0k!?A{7O+H|l(GKMtf8w)_!u7XTGdpE5*-~i z$C_t>k%czA{mT@XGD=?1tWkRM!J{M>Qa&7eHe@-uJSJtClCmc;g#oCr8Qrmtdpo~0 zjATE3JkA0HjD?nqJ!hvEpWW^Dv2Si1&XUoGMmLN@$rOO}vK6#h;~fxQ=FJu0$SOw+ zMBRQn@Ed45S*0ROb>Fdaqg{PbTwDyB@q-^82$}vA2Yax>*1d26SHM%BerEbj7Hq##{!3BYkp*AxsbFo)glJ9Fi zQaek|$9;D7yYWWK#^ z=jSKPw8q6m7lz@w%uESgId9k%i(m`AJM7RwF3)RKzGZ9nY~C602DITZ=vT0 zak6Wh!+GcaO;^I-1v*FmnwQ;?=}(7+$zYhzu7KAQ=3-3MPY>$h^v<`D11#0{#6JF! zrQyl5FLU~mTxQ3|#d8(y#6Lv%*0S2WZN1OUVspqBAe}U}YK~^H&|;rM z5q#fZ*p2>Iwu|8E#`^Rtw?EY#farkP!aauIEI|cA>%Dl1d5Ea=hTukDYSJA|VG~R5 z6Rp=ZKe>*cel2%A#%`*FrJ3^Z#{m`eYWk|mj)DcWjy49+c|7&AHZLJU(rxg(W>!j@ zk&cQ2#29&{1A4n=_l~^fvzL3=RLO5R!^Nj^8?g77`SZ3%kTQ^C^eT=Iemhn_c9UJm zbtB{}kP8nfKAg;I=C?B}?8gfH*TYz=dzlanj|^nE?0vl1L6bdJ+_Cs>lia_xxBV*0 zR>kbhTXogTOAl&E0<%6ynDZf8t*C75T3hjM_cw$*&a?R{6SB;pikrt51IF)F2>Y7~ z*+eH+T_)UYE?O3+_);+FO#%&WMu%krtsAH8X%hEBoNLGfIy%NA_w!VPZ`^TX)Ms$n-)|@65h+balf^(cu`K$LiDET6kwR|4_iH~ii0sg|!ujKv zD1UETO;^xJ4nRV7`983=_c2~3wj+U#YpVOC(M*{Ajx!T-Ji*Vy(f9W9IqMcocO}nu zAX2Pi?9Av`q@8_ZR!~rX;j=u`ZYhaBtYUkD)s@$k-RQ?;96wjE8L)_QW7#rATO(R~ zC!JrhCtW5&%*T#T?ZryGNe!qBz);_fl3KRF zm&uTOL0z4eZH`g5z*eXdv}#Q7CnJM`pCwe@B5Fq*8>CQ6-a>^~a0PA-3K(iTZq`FA z#D{4GjGb{#jMqO*wKQ`|_?Q4Vzh|MjB#~>lyIc0VQ=wRAlW%rSw*01fH}yX-K`;I) zW+Px`r>YN;e@J@F&Zi=2T=NK%A-7v<%}2bF{p;u@O@}zcGj08xO*kYB>EWuyY_@G| zCpNQ@fcU&ILD013N8tbSEM`>N9%yxXEvSms0uTisk&kVGIU-l z$N}|}^#wQSF&yk1Nb`r^I0~Ux^|p27PXKb5Lv3=a|F5W$3YmI4)1O*-uhbY0%kP)s9?Q}6+3}|s}p|&G}N%F*S zSbXj6m!aZ}r)Kj_WuSQsWRjlV(flqx{>?Rp34C{3A}HITBN=^lX3@FN<@uQRZ60nr z7FxD9hxqg&t!@QXbYHUR%ubK(9_qb%fL2;WPg@r(p{lv10^=t){T_{eYd5A6 zvn5@x*c)+|hGn`djOgQUDAuPv{B>Xd3w~S)8y1D9JwhYrgi77s3BaTZu-ZthRXz`2 z{9;156(XJf%SFq6_h$rksn6yoQ%pGW%*%-1_k@HKfa;M872$b$mSNs9x|jw{00*){ zuxhB!$DSZJ!x}HTHp{$RYlRr%2JShyr`o9K&eK;STfxorqYK!7toW6gkrA6v0}RAZ z1y~oPth~Hjapk8l+8eHah%1`Pl+T%xfy|)OS6`X=Y`ZB}>9_w;^5X>aR*s~cJYql4 z2cTB;T3J1{=!9w^ucnp*Jl|GE0pu!*kbxS?I;K?f@@!eMH*2<3lt6oe(j-lQ8GKxx z2T>~ch*zySzFshm`MuKm?2>!DKZ53y;sI9%bF$g59}VaijY-%s=|6w}Iy<_2HFNf5 zYEa@2wyur`#kPe~i(`JBEvI};PD&LG&iy&6%VQd!#Ab&{;B>Qzx(yjzq}!Wk1+yV2 z4%st+j?hm`^6d}E4h`Ta*E|}z(RyZw<_<{j@yf?F)C%_MUN54%KtZkDY(siX%vSFF zF~_Lph!`CgFgF0(sXM3@@3S$K`N6^vUUa_`so7sxEaKd&(Ir2jKcLdn-k(i1UPaVqVQpfQMnj_L;AIS}r9* zfE9uLZ6|Fo*W%gIS>|dWID6Kb;^~HnDb?f_RliMTOkQsnyav3R=ohcap&VwGAKUM> zKjX0`BuX$Ff6F<*OCaHq{jps{3WrHOoD?o)*KIToI%+$$?3Njch|0m-MT5b!z$}7M ztU03_O5T4_N4(gkP3g4b^b!v=V--M>gFBCRjTIG zDNJ%wYGqYRr@p)M^YG7PJml-x)^FtQ=X)|AG*b8+7&B##q zgT}qFs$M8~HxQeYDCs+~Po7y3@CPm^%McCHKB6jCklDoF=!3Q(rj=lmlgB96{q*V-BQ`R%In{TYXq zmMbb#RP;S}Q3HqwLWyIgHAYAj0r-(owkj&Zj=EP9|7b+i{rtXgH%1<5m(QHh&aN=e zmxN$K@&^c}_TEs*P~TykvUzf23su$v)pJXP-%~P=@W2ehZ|a_fvMgX0NCc6(qFkf> z*4G=`kE`L#ASPR^TG8T@SI6)=6Omz42O^h4-``r>>qUB$dOyOUQ}6Re|FxLP9_o|8 zDiSG8Vd1l_Cp%p_Z(MmBOB{I=Dr;V)k zVdXt5QBzLnGh)z(b~;*Qocke)usxQ$4DvhHaaS!pJ^6%$gpBqev>xC;QNO$c&&q{z zAI(fn%T#-wxP;I3sa&Ivwg1MppIWrxc2`3@xp!p10*Tud_TAJ{p+zN@=da(0=KDML zmujR?4Y6pEXw`hq?yFkX?70oU;1F!@U#X(6jZ%&so`}>ibGZH(lp6q}Vk=21y$FQ? zF=pc!(T_NTOT8`r`JLm26#dTG;8?C&J3-@2^2sGdcxBAw&h2} z^$^CNlw=oH*ym<|*<_rIIX750n{o;BWS3oOf#H)s&z(>R!2!xQ0>OPCShII>S_si; zMNKxF%N*xAD|$Js!NAzN? zkAT2|7fxFYR~?&(OZc^6UW4Ny4)KiVm-X`JVYyOZ2W;z&_3M*=fA5!n*j^N{Va{ii zli$F^Kx@s}$yp(Twq&iPMAtov0i7dH!Cx`br6p&CJwb9HXEU;ef>W~I_WP>lA$(4V zLT-!4nPC0HRcS<4(fa%a77xhX)d^RxxL@3z+9vW*4eaTO{nlq2UEEUsqRBpU(sZ;5 z23;IfK@1tmO|*^AfrIkUEo|m+?#027F_43&T8M(9Yy>!T0IqbvV?sh)cCj@W$zl|C z{HDzcMrYhCe1^RfO|L7l&!_1mJB6OkDWP~8uDTRVP%I)XMpRmf^6tKw*(~yA3Di5g zupZYOnm2Tu+|QRgbPcdmS)`)Cpa6g&8ZqP-Z#2{ z7>CqjCnCVkk)1;8(214n_V62L<5(|$40&+#^jOt)m1cdv4EU>RP$wSG*4iTE={~}Q zMw;kBhXv4FvAt9<^8^)8{)nqWPcs){#}8~e+|?c}0r@Ck8ET^g4sbi5H9S`q zsYwP-*gHmr|~#94x0D%M;_BX_#}at7Aym@ps|na*Q^vRG|8 zg@QY)ES$35y~`fNKb>9g{p}B{)=JMvDfQA!9@*%VS0u@_s0U!eE>i>4`Z45ipbf;SMejkA52(vg{jxG(r#=#c>c)=ksgZJvr{@k%I+)O z$Puq^$M7qApnmCtqC{y6mwST`Xsw(lxs|5rc-)Z2s7?RkMRdgkwZhY~&)uf=@|M56 z4U$DA>_~XPJQF(|B%n1`d1XRaUF{@*FKQ}H`Kxe>8i?rj-o9AJCu;M)-zz-Uam+0^ zE%2#lgTz`gG_!@j;Cw^d*5-p0h=hd@=dRr~eMPSvqaI`aW@L9Fx4J;4()HS(FBjR$ z^wU(4f*Lq(qN42Hd60Ka1($itb`Bv;+h>slvh>@d8YRh6#DQZ?BVfo~qn6hS>HS(W z^KQ-e7Ezv0v{?5}xV@K@_fq7Ovd>px&Drr~vEsv&8Zn6`>J9JCM?7i#YIjG-Xb_rvYkp%>1}Q)P zS#?v$mGYjAy=1`Q+vQ_H8TXUpCqQlB)9Rcq8|zCiyC&Q6s&IeB38J*f{UUH6mN+$c z0JPl-b+DiY?TP}$$nJ_YJT2_j`1r2g3dKDQZN*U_LB8Km1>G0r^FlZEF)(aXtLfwb zlHZ(J33lECwz2UzqCx%U_ctDsuP%P8yFuDaOr_~jd%+ZEEin%A^%#hPxH>BzoOx;D z6jywvFWu*kJ}h4)A`wJo&@fOziBEnikvDv=o4!}y6_V5E-TKx}g1p!e1}e^6YS7?Z zAja3tR)@JH6J~i6bDC`a?1cbm99Qq<=3LNry;N#TuuGf{?1#EDKm+|QEz8{am0k}$ zanb3J6yOi>RS+ypcr<*X?+KDGei%Kw1Ea%;=GmS=S>38$NWu44mqyZ@yseueg~sSd zBFZ3QXp9C{m@_@sk=@)AuwnyY#G-P+#qTy^-Ma_N89`dt@P{SGNmDiL-rrdOIkNPD zvLzkvq9>Lx%k!q^W$+QmZkMLK<%(hVhocqa%|8CX-|dEQRa>-1)n{Rl=X4sZNGO&t z4+BT=4L2NQ@+KO5B6o)*QpVoANBwDZ$e)?Y3}rA5!Nh`*(isb#L`CKv8UPvQ!XIh# z1Ox2im)duvJ(goW=c;Q`GJcm>puow5lQPMdVdZ=ItLTvxV=wRTH!9xl&M%n1nhX*U z#Y9lrKIKsizye`pB4P-<(Pu?SRkxf6WxDj@S-75a5Z|Ap&x{PY=Np$XwN9%F2kRzB zczv_yvKR7+4veni6Cr>HtE{Q|Zhi-?{-T0$8cp9PPXJ~r$0ygc<36gYwrnuANYcmL zy#PgFp1K&~Kg`g~+PXmcn#CW3JT2$x7=_G^pmVQmTo+z577q^(9tjEa)2A4;B2+SJ z!pKlwD(JZb1lo;#%>|sW@JH-@F4v((=XKwO7fckH#bBWHZP6+aDAQQQP}p@YW7-F_ zT#VPim<_XqHG_r$TlX%BU=PH{i?>*Aupe!ViuGQljy8?eTFgtok(HJg2OBfm~Gb`--j4 zTs-p?GLG|{a=g_Zk*hwHa;Fs`sqI$p+?GFEtM^*RP;s_jTBTX4UVhC(4hlibmgEWk z?)g2ZOUfYg@?q6RyoO@A=4Hw* zAIOHi6)VN(OJhv>Ao}f~%?;W}Y0c|qfbJ|4ay4}4yDi_FF@^fn;6dWH7EK?-{F}#+ z$YxODA6ykVxerhnHr4EM0472B486d!vwmOr3kq@$u0+N8Plwk(J7msi#NdsQtuLML zt=|~T{jkY|7PjNU==KlaG1kXPWEZbE_&Hi*BT$l#E*Imj31NX!0}CIUiHEy0yHZqE z2Z@YTz+~ao@pS@|V0MMfX&e`vtDwK5@`Z44R#n0BWwr95>XhnrSQiu|5}*&s3F}K6 z%!*HqYrJ1^D@~P8nrhZNfR6eG0Hp|`xy!lZ%;rv2rQdoW7gQcEp@ z-?>+`V={>dC}Zp2Kxa9ODtehnzsjS_WB_HpRbFVE`q=x6r~BbI>wG$ZCXR&(Dly3M ztV2WSLh}#IMp7#e{n(rS8Y7&GE%&B&HI?*Zk2RJ0?OOH^F+Ojs-gTnG_`rkNF+LTl zhKHD?QZrX647v1EX=_ibzf-BZnNjWOlHr(;*Idm#>8jITWw62_`p#$vvF-^a8&BX_ zIt~|F@Pt6VaeIIH0&7M!xBgmVAm4ESO+Jh83xH(45h9~woX#g8cYo|RFVC+|ain0s z`&pP4jWRjd-h7MEAEcgS>P)Z<$Zpl{CqIfZ=MTF|#i3ie6KfbFoDiveRj~=&40JV7RVcQoO9(`dGecRaZL;B* zsYPi`=26#60Il_GaxWVlt6tR#e9*4#x$L^7(HF52E>0Fmq;1}{nG9>wZ+J+oJ3mP` zV0lZ9hEgig`U;`hWdn9y?r>S@k}zh%N`F!EH+zr^tTIzuLRChW$Kapc$6fty^}(NB z(5bBLmF05d+$w>dd1%iC_uDs%o{(@b%oVc-0{1w(tZbC`O-kI#YbN5r9ELDco&k%U3zxDzhjO zSU;nI#~`n%0)iZvT0cgrpIKeOzu(2zqT)1LxO}gFmw2qlJ-WJPHWeQK2X7U0t4Ri; zVY#~Zhdha?UTqR?yigWXIF;8OhLJ9kN#<&HFHi>Opqr~+0}6Sdti`&;i$TE>zZI|M z9}%g#4N;%;ITM~cnLU5Kc)j$7Vz^n@ZP9w6X>G3#XvVQ13NXuO7t9*NHTK)i6dlls zz%Lpr5L%Am_4@K4SYl(Jar%G7s@puHqWl2=G*9jnXP^!4Hn1ljo^U8C+xjii6Hwi1 zBwvX{^E&ghU=PG1JdAu91#z7YC}U2oe86C}`7C3?Q?E7hYJV!u*RCEzdrx$Bu1YO( z06{qGwj@C!PJw*aCHv_NWNLqF>@t(?#|yb;3{!G2mybYkxcuCs6sS5T!oGWauOZt) zuwqbwzlP%rT<`>gE?-lLlOm!@FCANh!9<9%KNc5X#*J97fAt2l`!MI z9uYZ7|LA}BgoOB|@Wh{KL0*zDEIP5yZvukA=+`?v-}B+&4@@++#eh%Q z6JVa;Ik@s7d-8|$z-16ueu;j#i7df~IR#2<5YKR7mf;SLn2pU<;)r9c#iJ>H--!(J za>IFs^U@6q{tPEc(na#gqz%wW8r4>V<~}fXd|^&=W?xxpsZU7tu;RPq#dEcKiK1IE zsCFR_NG#{EV?0IIdDb!^alhJ1LEk8M(l>u{V;zm#*;y{kPdNW<_L&XUTzymoUdXq1 zsk)j_x#A8J7WtE`k9T>C<)UB?_Y)3bh2sN;v@cxSq!iyY9PmxmZ(rYoTN}*wBMfe! z^FOm%(btgRR^yLVDnouyfC|RT3I!55E~KtdtGn$ox9jv!qkg_zOeN+WjYU?{OwFKQ z%oeN><}=B;oXUCr;`PqFP`w4kI(JpooN?zAUkwhuJ7cLCB{^laZA86>BMq2o`K!iO z=(111?9=zc?Ld&1*#Ew^5lMnn9m%JvzdQa7Y(Pr>4E)1&vl-tA=k8ht_x^$+=M16! z3x~7m`J2@&df|81H(MRo)_ta*>E{>9N&}uhM0t*l9BaS+;72ED<5wh6k}4GXk1mF{ z9|dg;B;0<;MZi_zOs&So$bgpO(rv?xDk=I%C zg&Cpq9mFkVRW!Aw=w%ActlkrovF2AzvJC8`r>A1=I3E2nX3?2O*RbwVwKc*ZNBQp1 zt8xCJiESUq1BJWF_1y!Bi1&|EZ5~TCVk;5BtW?6g_b@*4HjQGbCK}LwB1m=~MVuI4*6Q=(S7IWSwW(r3 z5WM#Z6{^(uT#n}&JT58{W1N976rI=W84gf%jmb0Ckzu(2=5 zMuCiEMOk*IU@vJWNA|QB_eaoqzrcsxKpr8BavdgXFkA)MnU+IWJ-CO?=8XgK(MZr0kHE*_Y=`|5F4`zrzz5=FEMD?eJ3ot&|~SQqyg|T6Rk(kZEpJ9HoGFtXr38 zOCtjJHu1wgyAcT*B7!}YkUk|US@eiL{?$$CyKA(&H*!oV)aP}5a926?kpyG>7R#7W z8ToO%ET!J|Pd7;DXFH^4c}|R>r*LEipG)f7(#ZXfUZSC14G!<~b+e{M6t+90ex|Sb z7G1u#;t5e}AzVK@KKzPe6zlFcokfJ6bcHNk0Mn`Ny8hMN{7WeByyer{H4m+JI%3Ho zH-^`aX2RTd!4+2EwzhYVrSHjVMv+NhIB6Bnx=LD3ODWM##tfyvcX|=w6E7c+ZGR>1 zACi4sd;jaUA(=An3ApP~oE9?WW>Oen{dq%E(dzrnTlNy3YW^Te_du!(#Z>Jg6bB56 zTwWJU#Zmm^hpYXhcf$LTJ>6|<(dX0_%QTdAGT2FQr(}swR7H|cunN{IEJ!D9XI1Tw zB9`c%ODrx-LN9T(fpy&aWy9(E3`b@y%BgBh2_?PE#iy11Ik02XuyFcg7W>YxEd@;= zwQEvzaJ=FdXO{vOSi!#%4e}7aZ9Q0JtGO3g|%N1wCKuwIm*keYoVby9rQJqCf-GhIFN*4!F zZDTF#anTQpdmZFg;G#QZzejfDh%bxZXuQ_1yVLY!z&^G|>K;ZnkyXg{{A?NZ|WmF>*=eMVC z-EUF%sQkZvLgL(|`0P@y-SVLN#>&pQ6Y}184K8vYB!T-8iQ(5ZIl4-H zBOB%m9Z8S*p77Q>TsxpE)~9$m*N09a7I$6~;TMY-%{%b)9i=9{Eh*U(*0iiQ$oN{F z&U+$wH-`FgQpfX*mChx9bb-%dmL$Y~r%=+J7VC-kD*`$6VmQ>2X0@53`;k-2oNwoY zB3x5%Ki_%K@yRXDu7AjwRIMK!AR)n=6KtC7bf(iUF%99f8&mK6EaJ2$vl~zhR84Qx z)F@?0Mhy#Q($P4RPn7g=k0s(O^Z{jTuiaNBCMh^ZSk;!i0(v3oBWf5|^3X;mBSP~j zk|d0L`7WbT#Nr5(_e+Jgg#1F5$xNfWf+&?eg};mN=$5YnPsYrXiO}vCMroGaFEkdS zi7Ymeynh#$G!)n&@fA+EiVW)(REpGQ5rupHK6SlVfB606bLg|aD;~ZVs${IC6`yFV zX~C%;Kn{4e?Q!T5u=vyc?rp54vgN$nO7By}N0|6JYVn!ktnFM_1a#@j5_I*)Q|~Fi>wQu) zsm4AT>=JZ1tsK}J-IXnnaC@db*GY6Wfaz5Da5Z;ZM9Q02-h9z3`~=g7x+hrr(f8J+ z%BPGcauS~kuTs@+WkyJtl+pZO+^xk;iMZbDn-Vi#Y6uL=$$yoUACc%adM2-y?9q}G z%e7o}FR|+knqTR5vxhX2QZ$m=xTX5S#i}eo7 zr{ebJHa&v`{E^#h#Fa>xRS$rs3hx?7x4muhv{1r`kcBwd=g<9xE$~=&GuMSny=R@K z*L!{jZ|tkudq3&TXvnE$-T3qn7Oc2kW8^KWRD=-{7+*cyu5rmm3zz9Hzj%w}V zglITpH4e@wvAQ!!ef<{KR)rcqdb~s{yVK^7mVxHFBhRWI4*LVjs*8NtY@VNW{rz_7 zZ*@H8y(We>{GMVK>~Zli^SjJH96z;Ov?9I)zdbo&qd&^%u>3<5$yuRV^pH>ph|Lug zKTb3PW~B8~;Ac7shn~MqahtE`x^EUiJ&}&ouIREWx%hd=t{DXjRD^HJD|&1W$d^d? z5EZhxMTxlAHz#w1-9IZ)a`kp;g=3Hz%nvZx9_C@X#eUm*=;X5jN1rot?uk`S<|I z1&+s_@X@(UXxMIi;@R&08aH3*g3*brNSfe(Zw(LpY%5{Eb@(_&z3n*d&GtcO{)^;( zWlDCp$Cu+KXAh-T`@iEn(6!;gZ>W39V;W^W`r53!mpT03Un{jaKD}B-k+~8}CdV~- zW%=LaRyvRYx}Jl>FO8mQ@3iiF&)Uj3kF?BGB#5;=Y=-7#a$Z+J%l$7$@v7WzUs)!7 zNdvw3oQjVZBdQgi|NeCxO&+#|StTnL%8W2$9*$S3ba$Gy>*zNc0tYM#-K->9Qzw7U z5qSu}I;2ONZ2pnwFE~$gt+*Sz4Bh&jAd?c;iZj)Ntp^gXxgY&}pqa76Hi6#xci2PT zSiJsg*T^EJPX-MpoN`>yueVMLD8*Y`CRy1D(M(a9z^qP@H@zpru=4(glA7=b;eg9b zA9w%y#C^LW{P_5I%7FdNk4RewmclpDo#5$7qocb z9p1{kXDxNzvQ}>@7tjs6w1B!y3B!K(O<~J&LsRdT;>=46bp1=s>a8NpvjUNT(-y5L z;j$#i3IAD!-|?|O=pE6l=f{hd`=zfm11zHVm3`&=#W7Ky6<7Ig;w?@UJ`bne)xYzE z1!Ded;bFGNf)# z*yJ?2_=cB%HjHeCvSK71q3nq?`8sOzegv#lrXczyN*RE`e%IOL0Iv%Dq8Q$@o$mux z`3}W?9*RpBDAikS09VJ{oeKC@aKAaFRequ5&eTd#Y}%>PyxGi9;57BP^y6r%+eKAg z_Bw6;82(4B)6pS!AvN`q9`QY%HU}?;i-BmD|IvUx`JhYaTZi8>z*j6s>{!mzW_RG^9TQgZca{UP6OnYj!$t*mn5UHn+I@*Z2tC1`6nK4E^!$31`3I@>WoD#Y{z1nez={ zn!ZGK%d5BO&9ur@t{mFFUU*E0dSY zzRD1Jh;Lu7scm7UzOfpHjfojTzq`F@y}h!|=3faqG(V`YlBF}FJctW;9_8Nq3v5HG zHyf8)^ETjaJWf*?7Pa6^%=g4jnEJNI^_2Dc=LjKq=*r4FMd;&i7qtal=NZw)%PQh9 zU|t$}y0p>c4rY=McD3HC-}R5-ZQ>Z7)>Fd`7|jLf$UXNa_DS~(=6VGhG=NpDvJNqk z=G?=TSBQRBc1=6zg|@~z?#V7QVXExe~jweDuNw#)02~<171P z`Fv;C*H8_g;QVv*1HZf z7Ewar2wMKX@U?e7YgZh4cYmLH939^+uwE9`d$>j*{*ilK6?##Izn?bPYGo^V%c;AY z!XQ4m^~!;&PTQ9Pg$&E&&)+YOL?dDc7Kb@#0nu>45WS5mAuubII%SOnjOZD5kbxc1 zgE5iRZ=R>~fGQ1fotB|1%2xxCPInk%rl$I0u%{Bnh_wicNMHIcB)4;bROkE^wx&rNv`>YAOs zbp64}!e-bWoK`Y18+zunuVi7eC!DO|4acUCG9W1@e`SrrX8VsAqN-amJQZXVRbtj? z6`7|r`r~=t^9CVk%lvGzrRYfSsq=$^HdB^2I6d$$!%Cab(>4%jzn-42hV}5j+%8rl zNgoLgJ`RHaRRY2?Dv-s8B>!E^Rc0FYe`M?SdXTpzGr*bvz?dSai?>fxs>_~VG z*4G8kL;z=YgX1JMng37mtI(It8q@t%B&Z()>NtBKOOw`GaQTv?zXxSynR*%ePfOL@ z-cI|*Pc;dBB>jclwFeFULMt5G%@l`(k`EfJ5_onWooO7_gN#isFjE@r-@w}ge zj0~&vdIC9`M(>+F#Zk}G!o$%5<%wpQwT_seo2>s0qhlrhKKnrRwpuWQB^|_l@sDXr z+M&1A1C2J-#-w!{VKm+<4jG>v+Ha{}HTQ$<1rD)0z(%t787U}?ebYe@Wd>I#qJ)I2 zKf7O=X~JO1LDq=+8C(nZZy%B9&I~I(kb${Zs%d9j<#eQWD<7Lg7L}B{R4zDZ_Zqm| zzaM9V$=^_kzM?MMQnpaNn<&<8gr9W;D7asIU4A@L{kJVjpS#Ya+{yj)=L^}~laF(C zk<0b2c}YH1r_4yGkPmbXG+3<=VYHCvpYm(43y=NHKTDRNYE~NSH z9nzOghC^Wim;HDk90>#;GfZQpUvfmA#(6kKk!@27XqJCoZeCb1)|cT)eqw=tWv}4t zB|vPb;bs7U+QJS0{wX;KU!5b0J-0j1RD@jnThXrO(aKXp=Q_sB-WJl(T}nW-ET#jd?O9T`qVr z?LFC4THOj`n+)xq)p76&B3-h__I&V$0-s(OEnM!UZWJo!n3Snmaqg3LJ*?ToO-GP! zRa?(3U57~-Gmd^4A!5-&MNuwUMj5y$Fr4)GY>vUV50D(>i?hq(nI9_)r=hpP0S}qF z-NkgDvn0A7?;o@sx)s7N#JgJ*ctIr*=a$eU2D&@f%-#JD2;b=K1ks2fUJKD57H65U zM?G8TYN|JD&(*G$wnzo>(t**+(N_)jwaUf^4fe-^9O&`A%Af+TcR!DZ7Uj-4(<2gM za*ADjl1wP4dcOP+493!0UY#$WKgRm0w=;bhg~GyJ(vfi`!h=HROO1$umQM;{v8{v2 z{{p64sje>-Og1Bh#+ZTz0=@#SPjPxfa`;h6vXIvWAd7m4%D_&Q&X{@4>Yn)d5dj60${B1tWJ6> zTe2mlV68EC|0cJO{fIA`J*gl zm1{2Y35gdr=5dt#w{<5|EEQyS3lfNX_^q}=7WK)A1Mrm3BS;R3r)Tl{_aIJazs`}G zD5P}n^HOszdD`N^9GQ0ldk>s;I(o@>-k&2fSTg68i zhLK$|16G(Fnjf%ycv67g3*Keee3VSh6!i|yEL?{$ex_qm;O701L>`&AUw_p&es37F z?7GFhK#b|XpPi9dHZ9gDy>6LiEpnyL$S*djtg5b49ufb}HE>~53~RE*4;JJ%9*Uut znhkg1*Z*BX+YAb0qh&*Fjt{g%Z)#fkv&6-E2?Th?hT9l_aB-#*DZA>ly8af`R<>H* zz*D0b$HcqZhU&<5E=;cvmTcVjugD(=ozoL6cZz>_31E^z84&3}>JkXmnhU~HUI*i9 zYgv#K{evfdrwPtgs{EIC%lPl;-2I)*X0pOA>sk3I+?}EbPF&0{7KJ0^kd`nAP{r%|B(a` z4>`y_$7^_?6=wjR4O{nCr1$(`*ws3ZPhC@6CF(RDr$DRqOB?xve%n9mZzgLn+SD}v z_`pN338E`#fk$w~OJKGzVmo5z;bok2`Lf@JhIznDrK`|~`B&7-ux~8WrrsIp$YO`y z#0QE(r+%+egBJmL;%u&{FHZ5D&GO#E;L%7BF@)aLI%5tbM|OJfR$(Z7;RSJf^cN-# zX%Lil`uiF>|2EiOgq#~ig%DYvu}%+e`DnHTkb@Hr_UhD^#m#uv#&*Kq{~GEkNM~0t zwLb8L({)fIx}sD0_%HA&`mpiwD2yR+vrhM-^Fb+nEeDLqZgXzRHUmINe^kcaW zC7`5jR|RS7+%;{^u&2pDVX z)&wGagNuh=&zfm66^mx4aW3)qoaLAPxr~8}-s53F4Of>*iN?OP9j3nQhnF@qL0MqB z9637zgZc_B;J>gZMnM(+9VXX%&2>%He{YELSekw($qUue)GYu0{d%pwx5HVk3R?P7R&Hil}94sQ6--gBQ2;UJkb43BV@TvZXSExjf{?apflLzEu z!L_`$ptZd`(8;gkXGBC(fDNn*3jBN--vqUaF&?*KF^SF{TmFT^`NR`dOx)OA))oH6 z*1-qwbLY1#e%R3kuDhhT*x}`BDm;i3GTy9MCJo;54^O}0_yE@+GAmwgC?ijPHg6#k z!et`}1F1R9;`C|jl>b&D|5YTbsI$ue(-Qr(ABSFxt80a><$$k6vWE&^sVuk89!Y{Y z-{QtMwj6cgyIimn^5Sx=7i^TAPY-3OsGdMK;>}TsA=G$WC^jO=nA@e(Exa}dE}PPO zqS7lYvoK&r>@@hun`x8FZjwf=_!1`qbxW!8MwkX-0#oX^r|kKs{b*;NQ0${5UK#XZ zu``-r=*cr)3HFu_K{+rNK0S`#)EcqP+0`S~u}XZE?L{pX^kAtPM(~P13vfVa<=^B8 zq@{*|uV2_RXo-gd0fNCd;|1q9CZE*6=w8Wz1YsOS6?SC;_}c*b&NDEG__`} z^52!^Y2{ba)z`NGT{i3a&%BLUNY_}fMAp8kQ)9vIuC)k@X)~TRlZ%wTTLlE%k~OM$ zUNKyk!Yh%G4}>u3F{}x1h)B&ES|Oz$HJx?!#KoNwYktKd$-2Itq-Y9b>epY40F!YJ_s z|6d_d{I=@cO<~#&N~JXz&vpSv0tYoI3th4@H?WyN)mBw}N>e$gg!KAqfg+s~xc3j9nVxy+b!nx}s3QXEj*KoA(^lyr6 z3P1JIP2EY{5G{|~e%lt7@=97TNWPa7+~4OAEPz5@r^fAYOv0&uw`%6f81x`f$%=8p zYdfD0BN{L!iX1hHfvfW%fAtF2Te>KyfLQ3w_1XNCVJET3O%Z?8|q?)qr1Zn*|y zC6JCod)xTccRJ}PFDOk*MiK+n*Z(puaX1oJB0F)oadu*P){14vW0$;-Pkz`Pe>Ni` zZLW86w7t`j`HHANbnFUn!tt$6kHWGM9sfwOE6EoQB2m=m)E4YhZ?%V1ypn;lsQ$2C z#mOT0SzT2noxE3RU3rS%L5Z4$Of;$fUSGeuqN3t>&wJ%I!g+>SRB*FpTLfSsB|uuh zREN_hlk~vhjkttBr*kt@yxS`&w$Df9f>o`UZ5#x<-k`)O&3^^Wi)BWSZ>BAzM3KNa zG4N`kpYYG3#;%k>4{j;uP2|%^$rKyzOzyIDTSO(uwjv_smIyadS3o}t4kQ22y8eRK zSYix-H%XN6wTw#{xZ~9VxkB%UA{YhJdP(d>Iz6yd10+3|bD5ZF5b+Ker`JMnyeTNj zhUT0}f+7wToVW`Ys8o4s0{Dd)Rg*a;3u{Mh9$YyhLS0vXT? zPIW48JJ$EZGGGRkUJM<=PNJGR?Z~K>rI>ON4$q625i1CJ{OMV`g$H_!9@8)^14pB> ztV}H?#>-KRj%!5HAy#S21mT9RZJvMMnyN5uzQ98aN|@8KjATLZSArcBe}z~fe~ zSSNcYcG__?wl}XL0DSd7SU@%XeI8vJNT73zR68WQm_gW`Jlp8Jwc6Gp^oNXO0j`&o!AC2Kbg%1n_g`<0f5zAB zj_4&&i;}D=0Na`1GHN7B?FrAkaoYQgPCXSv@ti=G$Or3645V&rI}1)1@7!V2PA_;W z=OD6X(%i{1T7DLYX4p*(d_bong(Pj6rf9q|cSQ|~P6oF|gfDFJwra}?yotrX0+QZF zyuwuL1q6zH9a*1cLhr2+EJdUN%rq7`Ugt8eZHN3gX6hyTg&QgmmL`!ef9*3nl$T%MM>xhOblF> zmeq0CSEJZb2K?pBuUMh0?mj^q4N{AN!zK5eVX~^Kam;LNUFYPxL8~Vj32Cu3a3R3) zivZ(QI`kB7>2#wa`X4}1!{I*Js`*t7gf4jA0Pzs=fjIJl2T^0&*uti;bYX}sY3dCL zhTzLF$iyBy2!aS_1-5eYZ5P>O(eH5JS-d_bb5=<(Ed5;0gke}ymy{^oh6gB7uB5Ay z%pvn_!lqLuQ#d%9!D&yO;`8}OMFv+WY1jO599W9psTjS{3TzX)rsnN?;-f)(Hq>rW z%NAO>&ytXeQECC3d|UYLi^Ivep7uWHD z{Ag}&*S*R-FF4##u9v&2YfWVFh;uvEOXl%O2csSXV7AUj*U_6WBJ+z=Oc*|7cA|u zZ_rh~R~rHzKe{q8?`G5szpRoQx}j4Qy+zkHwbC?2Uq^cQW6*DL8$)o)en3E zFAF1Llmza<&hwb3=n$XKvMU|VPNk!!GI@etrm@LRZps^JfKqs!#LG3jw26O^N4LA& zGJd8X?u+Quh)TmJ#!mbM7Ukfl)H(rb6fi4K+Bwdz76aM8qiq+|?3TwCC=>9WZeA&D zkH01q4o!h2}pT87)5*2a|`6;&?W-~U1ry^_<8#8$K{3<$<}je_WBG~;ua0~X1F zSpvkx>WIJvh?(#_m}Ya&vO8DlRwYuBw5RktL#${*)c3vm)E>lG77)V${kpob`WN7? zELBWq2xgx!XJtSR^wTpj``+QY5#OqP11}rm0(XpLt6D__X?4aRiqCG49F+3@Oav0D z@{u71F3>^eL~m+pO2x9Fg}8T%nay5{1G+4=sN4zBmM7NK)U-KW>xy6YleWL|#(bD& z+TO%%B2k=41{xpm=sbG*e^0{SaC?gbtkd^-ox{>HtwaKrvY=0B$?rFJMWf>))VGsv z{mrJ;&tIW0DkAVkC*{Ih9HQcZSsjs(HVC&+nRWht0m+AxgG-Fdl10ucL(}f3e-9sX zEH4$#?yy7S-VP7oWf`yYbrNI8QHpd8=*TWgK&AEvtHUOv!tUuUhGVW5hj4|5GxZZ z)~eP;2EJn*2B3L%ODLRT+Ah#O1a|h%$qH&43OB=dFEjv($$;4DrVcBNaeHCqwh0yAmfb&FCh~zimu^MnePU)u>sF2%1eDyYj|Mn^Zh(5{5 zo-@uIxe}KA`RDP*#sn}sjtfWp17sQ)JIp?LEQP)*Z|9ZGfC5G2QcQGPTUoy zKn%x=F01xYtK#bqYb+c+U*8U3xH^6zkD;>OHNMEFlv=605m{2m&BGPOd0lO-DutLI zvDCN`F83{>2r+sdkZnts&ToVpk24gQf1%spN0fG?8GAb@V)5}cwJIH=5?Xdwcm}Qe zy)-F=Z3%$^Gcg;-R;sqZUFXLlsV-_KX)uCI$}Rw?D(1Cr{r_+dO5MNem*1u~n@RqJ zx=Bv+kY<4*9{9LgC;}i-Dh*IL@;j448dwftEhC~FZdutdh72YKSrM1|uuvrJP-5T; z`lzprA*I1@zjKk~Rk+pM1H3X*pv!P$F!QWz0Wsb*_$&LY zluLS!+&=1PD2dGMqJYS@@jImvriwc*p&&UT< zhtTqb+!cBG`5&qPCwZIB#LJmuxX7vXiCMn)Cs?SxeUpKSUzMf_F(C#R>9zr6GIum| zc#r`U{B;|U1%CdT$Z=Y5$yegHt~lKzJfjQ45QWUl)2WHZy=KO|!W*9qIPJP&C1VK* z(tXq8K%v4<)Svd) z1F4@ZMlck5SwZGRwdr&2@{g0B_l@QM9=8g-^`)SZ?a;SBZ9+mF;R%=USQc1}-?bGo zKR%i}^Juf_BIv-<#U5|xT=vo~Nf}6KlKkc?VCfQOj`1xBKu`d`P)YW~i+HFLzV{q)bjqM?DSHA2U84(`J=L1{(H!LjVcgn%8#k<0eFU{5qVNp-t`jV$<6A|S+| z($Y(%4>y~WjXRN6xlv~;Jm>CX9tD%>(N44ZML(&n_#|KfRS(AbV$zFxj$%^YUz zOU=161cbar_y;Gy$R6p)CeeO~OD)RxDuLNQ^i}0&^0BWejrX=C2GXJ>e+)bHZ_TG# z!Trm0;R-^p_qxND(E&0(0v=I&=5&?)**w~hr#(;I-LGCelEG-)ql^$^sU|QhhG?TL z$CZGZr;6_GwbXh!QXVPapKlg^Km9KxWk5tvleP;N^uiH*o8*h85lpCTJY;@z%sDwI z3RdwC%eZlxB#>1Rv`#As%P_PzorE!-&>B|9v8EqA5F9yMTbB#@-!~<;6ZI$i!Mnc^H?mh_z&{(*-7hs=U!9J2P@XdA*6&bo$L~p3B$P{cdFDdn) z*nH!ILN6{@amv&OI{9r%V+)~g4-@1(li(z^TjHUF8UuI2*e=ay1f=D&d-3t+a>>C^ zo0xB2`>z*}=CF9vb4IW!gv|EO24Y2B@mSa)81UBsv>}$tN4@$@<|Q9QeY}K@ev8+N zmVT*rHFG)bnR1&B=0M9QRK_(;{%m04ih?*upmU=CbD0~Hn|YX|(k4$4U1xGiv01g3 zi8kQ2V3s<_!Gw1mt=b9fY(Wg!&Dj`5GhM#;{_pX@p2V|BQpeewN6IhfKkN2WG<~R( zrtUH!Br<0qsn;G1s5H(|-#G}eu&=jh11tw0e%S&t&;~=#PK)#E>=FamV=FE%xor~w zg9x|#oJqV*Fa+mn^L(qlJQh%3W-TK8dc9e%AU=!gs2xd=b3!GJjCIUx3N z>38}yunim}Ivg%UY17a*%v}9daryBH^+GBdlq~fM+lo?9V!ICS+x;#I5Qf#% zXqm*w+BIGPDV|n#pcKM^Ixv$*AUhZCuw;@ghX7`_J#!FH-B6xD(26fj1mv@^$?tXe zwGn99(lftx!V%9JE@u}H(1hWmpFtP@uNMHN%dv+K6nhN$(a3nxDyorYx6pk&^nSKR zcNe=tr96A56qK#8)eRyXsEt74DqyUaiYdTY6vC*wK@pIwEJZ=LAq5f@O{Xl`TsG>H z1sTu+ehx=Z_Ci})2iW>t1}gq16^##JW==#g@c3U`+8QE8eaRkbpKgsj{5~f2Mn@b1 z{Ra;B5bNZy(R2t+C_fovXvby%v2rII6`Ne`=T9$(?^`Olc@cUSA{+Wc7=S^|N4pAK zi-^MHzobR8L1zjBvc3}4xnH;J`BTFo@(7U<=vD_8tA8*Vdn`dRJ0<_7}w9H3|K!Awy+2CwG}SQ<4^ zSRwGlH?$mKx$u&D^O06C`+{L80U){DX;33TFH3Zp=!Baj>vHf$v4B;m{Gv0&Z$4%D zF=CW}Q720(hYutQ;QOle)>kud|6kQ#WW~q+af{U%N*%nI@Z-n*#r)wpQU-E0PQIbR zz=NKDy;h+2nY!)<&`uiJj8~X?d_L7nl<35g8`|E@N60Z$t-kYgv?ZfrZ|HDvjrXP= zH$sZ`wy+)ee)rf+?z~A!$wU*LNnv^45Abb!XbECIaA0X25vvRx96M!vP(Uv>IaBjL zv-%&C`I~`CJd;#+d6OZ7j~%Llj|@;8OmihsO4%X-$aq8kjh9sJZ)cT3;djMvV>bDL zp46{{MhG{xbl6|Nem^wiv~=NjKDPbietoPDHRpB}z^-Qkfhguv(DL}01%_hAN`T`K zF$OZN&o>|V88HPh1NjQ{G4!y>gdE^u#Bv)OrB45dACr|AeGp)>U>a zv`L7Th`cE!_A!%0Jg$0QrHxqmO>Xf4XgL2kZMVH?z#~+unx_Snc3L$g@8G3ZSF%4# zyPxAlK$f0`V6rxV8OGM(`@`8ImZ9$Hhobi5l~{;==11dAV~eL%h~K}iz;aw*f1Eq=l($DO(&ZsMQ~&?{0z1&Dv_7aPMZd?b3jF;$ zW@Je)wYQ)lssg`BitQ3=;#i5+yXDp;gc0@$6fG+X>ejKr&@oTm{2;LwX z$yik@IhfBt3Y3bo81RDIO~>U35SX=uAflpYBWz*qp!KR9^^Ubay*Ou9LGCWUofJM5 zFveYT50($$&9oOGpm2Jr_}S`(NlGao2j#VvYUr^Yx%|IpJP=GSpHEt66lwH^LT(C% z>cFV@2Q3H*$iVgmJY|nTb&&U!@n^hw@W8Q%kc77C$17aiqLTw}f&2C)|KTEx7aDG6AwRjo zC<1oBrVMnWT=l=8VhfY6@AqP2|G3qF{^Q;)aeiydgjlp&YVW}1;6GrrfebP>Jt*D? zb>Aa*1VY$><-Pe8!@4^(QHZ?(0=6os2xn36eu>F)@utCxvSAZ9-bC0cCKqnS;?j}( zdM%W!;e)#RR!ID=kh1&biCK>S;EU~oTg8D;c7Qz~bJ=~-psrJo>0%Br)&Ki)(X`Wx z-;F<5qFg77q~^j=3e5QFj9vgk9$DTTE`+O4=86x6J1oNczz9XDngSRl#GPMIAROkhfpH1&7Y&3+LhZii z*?EOUPOgVoPL~(!uYdAldEBEypc;TV3Y+uPPhZ_!3S;LY_SYhT+*mm~73Zn}kr+$< zIY;`p30q(*!x#YLJ*W3Ly$$gss5csP()G>;SkY;xms4Kp0H8jwZV(3!)F<>~p`fgr zvjXosl&RF8p^EuMm5j6Om_1@BtvK+_BzJ8%H$w@`7w5HM_bR&d`gfZ1sy`_4jYmYq z+J;la`3uwR04LkXpsMD!&*xUYM1t;>iE#9QZi45i>PVonrVersM1BWw8kZ07;*P*i z2@)IN*xatqnJYp_J=4J~gJ)Em<8}hinXCGl1cAV^9YT|Ks_-%m-T*60izhoupkIQ(n*>P|t0!GQGo{Tyu zs>I9N(B=0nm=69k=v_*k6s3+DLglxp{^v-sgaW9SYvsk2Ox!;M3V7m|rDuVbQ5rJH z4l`xHHY#L}u19JfOpDLeq|yp>-QAljss{g_nICY?Y5^gv6HgGwbT<4-#OLh zC_YMfRd1yjjxj#2&`54f@vgOp@07L7588NHK0cL zg+{d}lDx#o`2BLaUB5R$o=N~D+ON&cX*J@rzKDT8NRZs9WZT&nIZ7` zp!7J1q%Es*+XZr-bfs)yfVBjYE3MCJb8M=*lTjV*lsLBR7nR>^`q z`zX>-CqeBS!mihXiLp3s14t$+2P%M_d|@K6Z4Ar|vvF{M`rO%z2{{5d%iaJ47^@m8 zoEm&8+Lqr?A1F-?2CoiGLdr>3`0V2>{qT&J)i>-7rspzQHn>v^9C@jnSv+cp5~gQt zTH|0p@{o;7t7#_P%876GIt=Ib!elkxynfE_YDO+>;>V842Z{#P<0^orc&U7Nc=#3- zyLXA_;Ra~AmHu}<>t_;>q|5&(S+rju$46Bv$cl(ADs7AIjL8UlHaI~AXdkNV)h*fQ zv99q3>qiKuA8?GRGgmem!mdpt;x;X@`v2izS#NL6_4nHm2!1)#ZgVNNvA6F!5m*dz z)t#c|P!1cfU-m*HLF%@IWGgoY6aeMpem#vayfeBW4z;O)HQSm$$2U6$+nDO| z7m*3Gzd3PXP`RSvafJ8I545hqRey@=$Mj+PafEksx3?|F0*Ie%WZFEL;%aE<2f>je zvhme0nPYxFsRG=KqWvE{W)27tNGs?PLu|^tAs9^^0pzD&B~D&OQX?|rJs+b(^Th`I z8+=|exqM`v?1@&A21dZ*aHAg!=sH;3P+#YyWS0FldQlQjOzQ0HsszXVNxUXU69W(9 z#%dXOy~ZmF2$OqifP&+b#rlCOXG~&=3ySt`9l-PFJ8|}Jd57Q9YW+r-_?oqGuRMwl z*jb=9pKCSt%l*v$>_9fW2)0F1!_b=zuQ4!Ci3Pu5bh_}1Y1fF#=+I|)+U;<@EX(K? zph9Xw*qeXJgU$n$I>s4O)=f`BDKKZkb?m+?agoR@`l0)fP;k|8l1|aXbWX(0fGJx19S0X`XcZzokJd zL0ypIBNsQiJx?6Q8h4INTyaY?=quCh;0)|q&h*}QQO;f1<`pbjRUBT!C2sp@39=Kb z*g&)w+y{(RqV*zO8_9k}5_U6&4N%FQGE@xAcrN|=?M5#F1|b7sE3O?}qI7o z)^V}=0WUALIE;OV27r4Wed2e$keKiq%Fd%x%~K~9-1 zp=ZgTEftp7$A*ra&>Iep)zm0Iy04_oHQkTG)Ne`ZK2rTQH@{?F8%`~&TD-rBqfsU5 zc6uz5lACOFl8+eu=cuw4#i_-|E#S22azhV z*JzcFsd?+#Z1f4n;~ur6?|sYi zdjm5fOpEagj;Sq# z%PxlxHx&G;ZPDXP?LjL3?sf2*`Q0S`_KE-UB~G({gLhIzO^s~Iex+^kDgGDnmT@n% z3?rxYZvXgSqMnKjpV^;}krPS~lf=}3iVsxC^ir+39E?-RHXgEVTJ$D|U-+5lPwkBW zq?$v^45q@obze%;COGQ&Jzg>eg%OHuQv)Gl{*QH3`@IgRM!MZ2}f4&2J5d(-AxU8MgUspL|t6{{y^mzy!Zmy`pvcEy$1s#(d$tzB!txbxwL#|)2qFDh&o zD0c)6(BN8G4`ApKiq+@8Tuzve)s;tx=?z0_Eg0`VI`lp8w_3<8cO1SHNv_-FG(D!- zI&m(hvi)fBf;9x_?4jD+vPIH~ikMUp=ZGz|cV7yu7Mb;fx9Ka?wbfP3wu8$Oj#KKL zejuV22$V>kG!C0@Z}**{1M+a%DjPO>@Lkh;ZX-8>H7wKsw@Y%oGp?WpjQ1Mm%CxM( zjHj6>6b$GXlL*G)#^Z^Go_2Q8h>fO1L3mz!1|Mo+8Ne8tRDP@SEu8&wi_&u^24b*Nc(}%|`+;aF1gY0KymGYj@pf9OR_W zehMjH6SYlL=xSU^K$|rjs%&BP8KIx{68PSpajqL3%j#vfslnaO_j#(Kg1H&!3j@37 zYD@Z}{F}1DR~XDW#F*5__&=D$-zwe5An8@C#+AAX<9ywmimeJDyhA z-7WU356o9MsL?~VNdH@4MP+-_IR>YOl^8fv)M2r}gmchZ>_HjToKk7~ZG=7*%pp}!&Kpq^=Wl83e_^Gm8V6`Yp1>SW8 zLO~%NACSx>n-C#^+4@Dc>T}@IO9`sy1T^Tt-Mw#g6Bz-tSg^Dr#3)v8BNnGfIwQ0o z<)Knd#0AEd!b+iR09YgxB0^WRf~>NehtZM89mvr z+P2hQUOeS)vE&Th#*adpIy#3TFMYnD(|r4G_2pb`ZD<3u@>UcwFwmb&jI;E-wNyAD zQ_~_O!Jmn)wH|J|T-d18h;E?p0<#%h+=IokIIz4C81$^Ik|472y;AzvGe(yHQ<>Gs{o0hPPWtG&y<vD2TpnYpr~U_jh$1oHP^m~HpZ z>z|Z(Q7VFDaQ%|dAif{CZH#n4wr0l##Vv#Q=rk*bXqs0m_XALhJYTJ6xf|aXdu>xk z)V?bOes=JZ0KJMF%9^>A`sA05imok5Ma@9UCCCMo8P?yE51>B__ouxMLULd+bR1kk zybn&5>`@oB;v8~gSSozBXc`Op%@+9xvJnf1t?7v@2u6~*%r7VD_o;1sujk%tS~7ON z3z{UaSd%rkN?{Mt2k(0`odNy%t>;L=Jd?*`_{po)fIJOwt^sI+NLsptKi{ux_R!sa zO52Z@7zI1#av!xy8lB1#ZT3&9!>GI2QtF!>~=H{XNg)Mb5d! zMYJ}LON<79Yy98Cz-rsZN^(R{R%RMRGBkMyu4e@V9>=HN_Q$+uZNPt47Ip89Rz_8; zE!y~uP&o)wkMP?z0E_JprFntWfk5bCu(D{gY4s11lE5?nl;mw80sd^XWr|UX;ue#__~px&%9ku! z+Ja_vrV*QUuNI?zx>0=n0+VO?2~@pPE%^LS#@rJP`9K1o3lSh$Hse*;RG12+8Kl@T zpKC3jNzBMz(p!MwpB^G#Z6E={asre3_VclG2T4r+xDg^{P4#4y=IRe^>b$)Fho-NN zs_J>aCay@gbT>$eAl=1VK8bySqbq=YGDwcP;+XwK#`4 zGf(Vi?>+q83^xock`-=%wikC0(Q`zmxsksqob~F}(tdmQ891wCa&Y4pYTev|EiL47 zi3u>FIxIHoLK-QV-~G00lK!QRw=wnCg;5Z#TylB_Zd+JMRl$)-*>}Fvg|hM8Nfbv% zIM&-TYc}UEcp-ZEIDMW>^WAF_sld=n%f+Sc#R2vy5gKY8Vn-Mucy-S>F(H#V=hio5 zT%(yc*EAMF+(Aa!459yG8_Zl%Iyf!b5=@y( z0V7xGXc6VzwJDRq(3NVY2UMV|vr8_etxa2<(*0NwoUY@p3+wj7bSTg8mc(E`h^<)l^J5gA>< z%*9^^XDNHDKK}dlvU>M~M6S5>ed_!9nrhF}XN5050VTaZ6AMf5B7uj};AX1NKr+}K zT{N8R)JTf)+LH)mBF~;j{eSSDN(y18oK$mgUXl9Rbb0GTKa=!{ESdp>1W4`9Y<9$@edwkX}IAwoqYU7wKsL;1+43~fU5frepR zdTdnwP=D0}KuJvhiaJA8{7YS<`E$z81r zvHL!V1$2xF8Dpj{tjFF9(Y#V5d(3`PV4Z?!3a6z3HwCYwF47pQcK zL3Gt)$NBBag!XJlwG;G)^lL-mc5mNf{;x%Xqr%2}QC2`p3zxLWlE21cPBE{qbxw?; zBT2<2w3rcuKor{F17~ukiR(xdbmLC;NxMyL$CcybTe-=ZQq-{rPTKSws0{1vyW>oh zyY>JRdvIxoItb#3s-K@aiu%re{=$A4Vh*6(bwA|kUPm1R1N0&*vEj!uT|}(A{|Pdw zu2T&H?61|0bAWKQF6Jz;!KeW}sWig|8MvIM)qiP}Q@qI912Hsk2z$U?l z6TWxMY+DH3@AJ=HC37Ir5WI|4I$ZnS`L{Bno%Bxw?IJBk_P5+@An_1A(`YR!IhupF zm^{pVDzM^LSZ4j-*X0Z<1laLV!MCy(lZif@q9Tm&FOfr_8-6biOy_D}D2~38>mopK zc3JJoo?8}GUDhpC$K9}9+qf3TiGc!Fli>}@_5N0yeyQZl9DGxbZNuf6nPTlK1FL1Y z{wrTO54YZ?0g( zTV~f596BpAGBUCPlZ>FWUvUdSlAiJLhWu`9qm$N9bAgkeHv2C(5-FKvsbodZ>DUoF zX_XGLBY<|O9bLcCM$pj!wxa=^1Bv;W$GImw8#a*0SN#es6*@s0S=Fh(DZ)j zYa{wMaVCUdF**?IRViI4By2GO9TpGeD}*D9W?aj-%_buP(l*~f6pN!TgBn!~X=OKV z4Twbr?A?RXe>BYMfiv(&hLvwoZ)-6m;oMS>h-IoPp+yqdruss(sa{w(gkeA+X^wE- zM_LKQ+&{dXzI-d7Z~zLBuY3D@XGp#y3BbR|WHe%Qs;I@>5@E&9RU}}8s7)f(`qiwj zghKF-*Hn~?xGs-`HtRVvkd+*rn2Yyowm`9i( z1l@R0K;w{ZuW2uo(waS5lgR#I1cn|Iu5;HX%CEyB`K76&0@6ZVH*e3fmxfggyB)M6CD! zXqJz{f+VSPXt7DjpWV$bjrV0ia~7hrYAjG#7hH+FkIx^rTjSUQXLh-rv;v#iKTA5i zodvdh7Lxu@qSXe66%j3iXkABlv5l)v(#w^LPqAlv*pT=bsD9X(%>y-xp^Pnv_tttN zo>EqTn?PrR2QM|+v#lM*wop&b$4V6xGRGvQrO;6C@qcjp21=4)Qvqg*A$P|IM4!j= z1mei|mHqJT=sHfi{zLtJCjJa0vdc|5bjIheZ$`MQB=NkRfq-t2|g4K}v+RiUDOLxL-<=tKBF zz=;#u18%q=@yG&Ob^ume;27W+YgsXy7jEYXJIaav?6BdI zt65SVkAaqO=UKN>V^A1JL)-oQK4D1;84HoO8VEw@KupBKCKAQ0o~Sjcwkq7of8I!0 zvdb96cO?I5hOXL0@i|&?UL_h(FU&*!)UTb=+KEZo1MMRK*UYIDT|*kd4bs!|ONb>r zv~H|f4BX=K_fK6LQN;y=Ez)tH+AxiqrTKEB72uXUnTq7EqXp2SRO8#AKVvBxJT;%f z^z$TB|0^HiUfJq)l>Umt11TeD6r~q&TgdigUmLd@HZxxpMsTnbL4SIBx}nrBEeu4} zw}fD~W4%s*HC0-j*Ii_mCkwC=%X+$eyg2BzWs%-6jH*|jBlJu=`oq`DFostkeQnB0mf2X7^O23-HuiyQRRg7YTY-Qe`I+##;ct>wg((kvH4j5o(4^q>u6UECd$5cG$(yA|1s;CA0Pnsj39v@cVBPxe{EO|vGMKNzOgQ_o0-_P7wNtP> zan#M<7iMflCfLsUo%Y$~rCrws9U~|8|DuRxBHOz3GV#C`)3d-K`8hMA-4O8P4?{Bv z{>PL9TX_aY3|+rtxW??@4VdqpAhvl{pDYi52EJOL`3{NHgMgU{6Pfj_Qvk|qeI zudg!EOeyH4v|$`OI;y9qFSN9)#oDvc9;u{2cHO*ee0a(=c^!Ff#(A>5iTtG5j#DWmoKOPG}t%@VtR_8ZK5v6yf^BxS6Pd=#gQiaUpCOSd745UvYp!;9F}T~+BC?LSu&C}$f|Xq# zTAB<|qJ}k^%k%D2NgXkVmp8dOmLENG_R+xbhgD2K7|US+${gXee-^6YAQqz$XM~dzp(-Ga&9k`c${J)5efdI< zww&)3g%Kk9;xMEg-hM~GesQ4?g(vP== zVOJ8fA?R;eLWK2<8wP{5K{e_olbZNonF_S_+9c*3X&Ex!6y6TJ&iy@LyXg{CVxa3& z*bjPUL4;&iuVSE@9G*-CAufRU>tk;hoyG~wJ$i!!WNI)Q<7BV?sXTj9KFO0{?i@RS z+jSs4Jg)gCz(|b6s}pB&i6eRk;UfP+@>-@0m~4{y!FwU0G{wW2*6z*3vQHO3efuw6 znixo+(?ucyyrc>Zgc@=P`^=j%Ht6V|SM@6&B5&3x#~;QVj5-*NX7YzD*)VG2)3T%e zPtZK(_#aC@q$VdbYx)dOovr_7!Cb-wYZOvmAh9$NJv<*Z9c4i6P0!| zc6qe}M;9nZg~n-nXUewyK(~JXHjx*2^179`{_2)lo(syNN4b?<{eBQh?Xq!K{a6&C|{YebNM1U9=FB$NaF^C8{bpJ*SjPt*$ForKDQJ=`Xx15ZxKFwQ!WPKD$ zYJ&*do|I5bvH|YLtFL2dRej=5xPGg*c1yp)l^i*O~8J8t=M@D)#3UF zkj~62nD{!k0f`GwQz2nI-TnV)NQ$d5%23~236X#6hLf6rKQCujT3{{i^0tI=%}+$H zJ#0Pb4(KkwQoPAK>vf!)+6+GdDvw@FByRp3Av*XHVl3*YV%ueOtI0#-(`V?06wVtL zRGHE?hoVEA+SUEb#MJR?! zY`Z@HZVMr@E_T1E7wAcJ2d=fCtVY`YH4H?BR| z1@bygW#t$@1R0{cs=a2~wqL0LB3P86@)0s;`lvnKxjDb2< z8@{)mW_nn8e^*%gjLw81$d2AZ3wW@lONK0d_q6sqaq_0TN2^8Rww*tCkxCB{<)@xm zF{|HdGk#nrY>X!D&FdVW<@`#6>?motF10~jm~0O&M3Fpe_^V@Wnw622ZrzHB3Y!>& z!4-&0m>U_61~bnb!o{vb5ajP=gXltKZKmBG10ow4fGY?Sgih&9`T3^E=rZ-oQ z(cgJqJAqL<3y(O_5w@nt6#f_Q4<(qvrGVBO{V^%W5!gr{@{_PvF*>Fq`g<@w zF$SIb0takluCOV`lS6q)iXCx)@ZR76Wq%S4Ixt^5{`8ypvsgUDB<3*&OhTFq+oB;t z;wDEO8mGL7^qUtxacrl(-bAkk|1h+TnQ=hbIGBdthVomiy} zpTP`85M$KSGonwu>6JY2mE_;}xpYppHE32$D5i_LPX(`oaAGf5`~^ke{Z#~~JlYxC z>6j{hV0aF6Be9*hF)RCxiZC|^rI`(S*#-dHw3U_BKS4UQY(go!D_qmWN=UFgsGMam z3{$Yiyfo^tKfeWL(1Ae^m{Y&6Qc{pG$>fWdY@|J(+nb;;03$x}eS_J689+G$X~xE< zE4A%Kk&3LCUUgOoH_>&nE(}2SGtPvte#TuCpPdTEjl5fX-?W`>L|%RfqB+Mw_6pDW z@lHh$Zr4&a?dZokf8CO!KI?%9$&;MHHmFW(sI^>|Cjv2ZT7G;gQKRjnX`QSr-T-+n zq@)u_QzPC6{s$1t(4Jn;;0{QTcC-NNziCjP4cuT;Vu|Mg7IyfTYl;NYwO4OHUq{w> zx;nvm-X9Ya#F5N7R*3%7(bCj}xPKP5h{_HRRaY`O(cE|+WA5||LP71u%#SrRG$gs- z^EE0JsEu?j=Q&Wiu*EPgng(jj(u5_bi6G}(fGc0ky-xr8rw&^=I1xljc}6}JeoYmw zr|+9~Ry-XsT_|H~S!z?l*?7kFy|6m-h0Xd_V;24YUF6aZT}(I1i1MHK1jfZsuLAsi z*r*L_?a%h__}ZzYubTdz-A4qdH1Ho#l;jjR8znz^S5$p?_U@BfEh!;(_$0-PaM8%L z%&BLYQ5sb6Xeezh`8q%brpITxDk;BTd=hXr(#3H(_5%31Jqu8mh_=ucX9-bE6LwBq zugUd2N2!%Ewo~-WJ^khc$+887F&WZ^w**@TmwB{$ltt}+t<%fjIdX7uVlt>k-|+KY z^Jj1(lFtZ|A`RqC)CG(NY6Jg$ZTXSh#?K56q6_jAcYSG``-Tst|5b-B3m$i66#q*+ zo$B!E6f-0gr|}@=-X9iYLmK5VF7i9_;U7BN19WVjuKj z8kE8z1(K{PRG!a)PLO`aHTtxa$Y?wA;di!+EOm?}cmvOPC~`U{c3tbIR6$$*D(KxT zcx>dmS&+aT=dy!<9HuvRk{_;pth>9v)FJpCQgwCpI}pEIemOttq4jb1^M$S=3uN00 z;lE3vr5*mAlDfaYuXz@P*cdJRCojrklf~;3%(23eF(~?DTQKwV6}qnbj{U~hVERC7 zAl%P~I@Z|yI)IVt$}cS*GyEK_b{+I%EsoR6YA5@b+_Ro1SpzKA&rjVLp@P2Or|07I zhv2yupf?aaid(0!8;u?md0sBR>kCB9~) zyr%eH%^IQtYF3V&wJjtPEJs1B>z-Nn}5bWfsw}5 zPt_%!)1&}DWZ*v3Ua_!)+~4DL_KiqFN&9R`mM~37!gA>bm_?)-IV-KLdtBKT)v#yE zL$d6JlE%!M!m^Z-ys*|9VNw%n2wk<6H&@P?nllZoh+3rMgRni8ny@}0)RJdzEh z#E!=lw8ZNfsFh(wL@r+Z(-*v-09xla?(u~#egXgX1Ml{C3*zZAG@)fLE?1B2kcPKs>wD5L9@56z0wsGEavufq+S;R^}jP zE8vM_bhm+4QZ7;K*kYHh_vaLKp?T>-?VM7S)8gFDCdf4p5VaySk|h5G@Jp3^F%tm& z_vv@)sxENk_lS};$udQG&~S)(z*2{ZnIM>SNT#dUCDP)uLd9}Y6M(248@LMi(Z@jl zanJx!J1$W|*O1M%IKlGiifbbJXoh%Irzez}=YAP%OsM zd-k;i$Skb;B46kfguT0X`4>0m;qcU({2`Jw8j8Dux%0>Q{?baz$OyCf6~QFXU9*E8 zB_CU?P15HmL!FxH>Fk^!QLJ1T|Con?+!_ zIzpwmDH!@&XD?1Ojnkf|mn?cfJ8svb%Iok(OAjl~?myr(yM}KFeMErgCBo|a#Tu%y zN@BfRyJ_$XI8*%mZxk2I7??&X({0k?KgK=(oX%!S0kbr$K zLJJ%dRF3$FG4-*O`la)jm0e6Z&@!9wCl3#4Wa@zL-h?ND7aN<<6fSYq7QNt5_!d z6Ya@^0#OyS|3lB%i?jl?S^mY67*?CSDD18D@L0H>XB&PBG+1K`xuQOlEmA|5JizZm zR#xV6Xa75Tur#8u%cxQ^5%fr_*>;`_jPR@=`^-*DTJ1;(G68risAhN7X>O`8g6 zTsqX5k7c&oUx*|H3$J*&QUGx8)#KA;iD>A|F0G1!IOUnw2$TWOG3u;F$|d-994`+~ z+24ojjJA*;ZX8}%TNu$GZ+DSH)`3Ywy@zWnB0Jz2Az`P1@pGV2^vU{gpWcR`SsYmQ z9EMf=lO9SDm+uK=;YJRnygqmv9}F6})xRe*j60g=Ga(3QQJhj`{!-&{el!9wect+q)9#ea1)|2`N_hlCi1{aQol5bUqe;Xq zm_T(=Aux{4@^wErHjen*j$*pBdQ$5EV=VxE?ABI`{tj3hwlM( zAIT7r_)8vc2Lk=)q~Rcorb0=~)o3QmDcOR76h~WqZtS&; z<8=E@L32Xjz>5iPK{6o-^6YA@V>`+=S5Ez#5cmao)2z_)#6$=SRX)LZa%`O`1s`TO zj*g)2-h~_*|Dz}Rnl|7tzm)(q!Q^Irmwn8X4ib;eq&v$4^_Fu&%Un+JC8Io z;Tyw1u;-#Mny3?S|6opqb$@@a`SvYNc!s{&@XG;Br$AA2d#yoOEd2-C(#4sH{$S>R zU&%+EMBz0Z5WUG;0#2O5=u8-cfc-Up_rs}bu5LkLOZ7YbcOF=qQ?xxj)NapMSU6~P zmjqvOj1930ceb?|_?`ZEo)SykZQ`(Rb0E=_lZqtYrQ9zmekvbNrqpiR6NXWIO=-Kc z_Wr%~czu=$$Br9#z8D1C0aEWa?ulG4&D1A@P$wptT*$)mpI$3XTC5F=mqN%+`zGuv zGbbsp<9d~>=iWt67F?^K@Gqa|dkEXp54@;Vv}BQ+MVIoD=ab<=y&XyMW^YK5n|^yD zTNUYd`nO{!9<0RQi~#iFN$!jlxZ7f5>Ik%^b(+{zykwSf$qEIymaOd(}^Ag5)J|K->w%{{t@VX ziECoJKf}r0@wKFe|4xiWFG9pVs0}Eu01B&f)cs4jzdY~tFL(Gg_}wNbAu+^=C6#q0 zgfO*U;PiimyuW$mbk9A{@_3z=fA-Hr*nFEx2e_fcSRZw$erjK=JxIVaoxY*ys}8dv zh>v9#s;=8*+JUk?<-bvol`$FJ*b#kdwzf;=4vw`P<(DW@ly3PA)MB$%gzI|w+J+-z z9+KyE>Nx-P4*xMz_;R|st>)v`6x3O_)D06i?wnyfgjn_4FxOQCw?4kE19*0bZ?fy=n23eGZgbXiniK1ZOs;SC8EhM}{(E{zXLvpoC6PA;KO7HvkamLS8cFSOxE z)DtoNJzp{qv+fMekq?*K1;gpNo)Ul;<^Ge!iO_06ORcDXu*r{QwQtlc*I}k7^o^zC zX)SxLgT;IlL zi@Qstt-86WQvUd%Y|r4s{}y2eXEngXylUQNDbPN)*Q+p>Xldzp#Z{YU-7v%uh-y`xQLlFRkGRo_*`zDToVVs@|~y8^wmfwLinjjzMvCH%E-{G z^(NK3w8f)w_Xxq@?f>mXd9r^aGLbj=m4cZkigBo>{(50$)^e7*?ta+a1bdZg zwQ{8ABafid_XcMzb@j6RA+485UkEUQPdlIe-%|qv;UVQ(sD;IDHbi5IXNHFYuHCmP z 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 literal 0 HcmV?d00001 diff --git a/Echo/Assets.xcassets/SQLite.imageset/Contents.json b/Echo/Assets.xcassets/SQLite.imageset/Contents.json new file mode 100644 index 000000000..8125df0c5 --- /dev/null +++ b/Echo/Assets.xcassets/SQLite.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "SQLite.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "SQLite 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "SQLite 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Echo/Assets.xcassets/SQLite.imageset/SQLite 1.png b/Echo/Assets.xcassets/SQLite.imageset/SQLite 1.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ab81926aae3c49e5d2f79980f16bb02d3b2b25 GIT binary patch literal 16731 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 z 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 z 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 z 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 W$Wb&s3nP&L0000 [ColumnInfo] func getObjectDefinition(objectName: String, schemaName: String, objectType: SchemaObjectInfo.ObjectType, database: String?) async throws -> String func executeUpdate(_ sql: String) async throws -> Int + func executeUpdatesAtomically(_ statements: [String]) async throws func renameTable(schema: String?, oldName: String, newName: String) async throws func dropTable(schema: String?, name: String, ifExists: Bool) async throws func truncateTable(schema: String?, name: String) async throws @@ -159,6 +160,12 @@ public extension DatabaseSession { try await simpleQuery(sql, progressHandler: progressHandler) } + func executeUpdatesAtomically(_ statements: [String]) async throws { + for statement in statements { + _ = try await executeUpdate(statement) + } + } + func rebuildIndex(schema: String, table: String, index: String) async throws -> DatabaseMaintenanceResult { throw DatabaseError.queryError("Index rebuild is not supported for this database type") } diff --git a/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/MSSQLDedicatedQuerySession+Queries.swift b/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/MSSQLDedicatedQuerySession+Queries.swift index ece03412a..87db7b11b 100644 --- a/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/MSSQLDedicatedQuerySession+Queries.swift +++ b/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/MSSQLDedicatedQuerySession+Queries.swift @@ -20,21 +20,14 @@ extension MSSQLDedicatedQuerySession { if let raw = connection.decodeLastSensitivityClassification() { queryResult.dataClassification = extractClassification(from: raw, columnCount: queryResult.columns.count) } - queryResult.serverMessages = executionResult.messages - .filter { $0.kind == .info } - .map { message in - ServerMessage( - kind: .info, - number: message.number, - message: message.message, - state: message.state, - severity: message.severity - ) - } + queryResult.serverMessages = executionResult.echoServerMessages() return queryResult } func simpleQuery(_ sql: String, progressHandler: QueryProgressHandler?) async throws -> QueryResultSet { + if QueryStatementClassifier.isLikelyMessageOnlyStatement(sql, databaseType: .microsoftSQL) { + return try await simpleQuery(sql) + } guard let progressHandler else { return try await simpleQuery(sql) } @@ -60,6 +53,17 @@ extension MSSQLDedicatedQuerySession { return Int(try await connection.execute(sql).rowCount ?? 0) } + func executeUpdatesAtomically(_ statements: [String]) async throws { + guard !statements.isEmpty else { return } + + let connection = try await readyConnection() + try await connection.withTransaction { transactionConnection in + for statement in statements { + _ = try await transactionConnection.execute(statement) + } + } + } + private func streamQueryWithProgress( _ sql: String, progressHandler: @escaping QueryProgressHandler @@ -247,7 +251,15 @@ extension MSSQLDedicatedQuerySession { number: message.number, message: message.message, state: message.state, - severity: message.severity + severity: message.severity, + serverName: message.serverName, + procedureName: message.procedureName, + lineNumber: message.lineNumber, + category: "Server Response", + metadata: [ + "source": "sqlserver-nio", + "token": "INFO" + ] ) } ) diff --git a/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/SQLServerExecutionResult+RawMessages.swift b/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/SQLServerExecutionResult+RawMessages.swift new file mode 100644 index 000000000..edd673f87 --- /dev/null +++ b/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/SQLServerExecutionResult+RawMessages.swift @@ -0,0 +1,48 @@ +import Foundation +import SQLServerKit + +extension SQLServerExecutionResult { + func echoServerMessages() -> [ServerMessage] { + let infoAndErrorMessages = messages.map { message in + ServerMessage( + kind: message.kind == .error ? .error : .info, + number: message.number, + message: message.message, + state: message.state, + severity: message.severity, + serverName: message.serverName.isEmpty ? nil : message.serverName, + procedureName: message.procedureName.isEmpty ? nil : message.procedureName, + lineNumber: message.lineNumber, + category: "Server Response", + metadata: [ + "source": "sqlserver-nio", + "token": message.kind == .error ? "ERROR" : "INFO" + ] + ) + } + + let completionMessages = done.map { done in + let status = String(format: "0x%04X", done.status) + let curCmd = String(format: "0x%04X", done.curCmd) + let text = "DONE kind=\(done.kind.rawValue) status=\(status) curCmd=\(curCmd) rowCount=\(done.rowCount)" + return ServerMessage( + kind: .info, + number: 0, + message: text, + state: 0, + severity: 0, + category: "Driver Response", + metadata: [ + "source": "sqlserver-nio", + "token": "DONE", + "kind": done.kind.rawValue, + "status": status, + "curCmd": curCmd, + "rowCount": "\(done.rowCount)" + ] + ) + } + + return infoAndErrorMessages + completionMessages + } +} diff --git a/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/SQLServerSessionAdapter+Queries.swift b/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/SQLServerSessionAdapter+Queries.swift index 9e03c6d29..b430d32b5 100644 --- a/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/SQLServerSessionAdapter+Queries.swift +++ b/Echo/Sources/Core/DatabaseEngine/Dialects/MSSQL/Modules/SQLServerSessionAdapter+Queries.swift @@ -13,21 +13,14 @@ extension SQLServerSessionAdapter { if let raw = result.classification { queryResult.dataClassification = extractClassification(from: raw, columnCount: queryResult.columns.count) } - queryResult.serverMessages = result.execResult.messages - .filter { $0.kind == .info } - .map { msg in - ServerMessage( - kind: .info, - number: msg.number, - message: msg.message, - state: msg.state, - severity: msg.severity - ) - } + queryResult.serverMessages = result.execResult.echoServerMessages() return queryResult } func simpleQuery(_ sql: String, progressHandler: QueryProgressHandler?) async throws -> QueryResultSet { + if QueryStatementClassifier.isLikelyMessageOnlyStatement(sql, databaseType: .microsoftSQL) { + return try await simpleQuery(sql) + } guard let progressHandler else { return try await simpleQuery(sql) } @@ -48,6 +41,16 @@ extension SQLServerSessionAdapter { return Int(result.rowCount ?? 0) } + func executeUpdatesAtomically(_ statements: [String]) async throws { + guard !statements.isEmpty else { return } + + try await client.transactions.executeInTransaction { + for statement in statements { + _ = try await self.client.execute(statement) + } + } + } + func renameTable(schema: String?, oldName: String, newName: String) async throws { try await client.admin.renameTable( name: oldName, @@ -258,7 +261,15 @@ extension SQLServerSessionAdapter { number: msg.number, message: msg.message, state: msg.state, - severity: msg.severity + severity: msg.severity, + serverName: msg.serverName, + procedureName: msg.procedureName, + lineNumber: msg.lineNumber, + category: "Server Response", + metadata: [ + "source": "sqlserver-nio", + "token": "INFO" + ] ) } diff --git a/Echo/Sources/Core/DatabaseEngine/Dialects/MySQL/Modules/MySQLSession+Queries.swift b/Echo/Sources/Core/DatabaseEngine/Dialects/MySQL/Modules/MySQLSession+Queries.swift index 6b74c2715..a2ec0209a 100644 --- a/Echo/Sources/Core/DatabaseEngine/Dialects/MySQL/Modules/MySQLSession+Queries.swift +++ b/Echo/Sources/Core/DatabaseEngine/Dialects/MySQL/Modules/MySQLSession+Queries.swift @@ -8,6 +8,10 @@ extension MySQLSession { } func simpleQuery(_ sql: String, progressHandler: QueryProgressHandler?) async throws -> QueryResultSet { + if QueryStatementClassifier.isLikelyMessageOnlyStatement(sql, databaseType: .mysql) { + return try await executeSimpleQuery(sql) + } + guard let progressHandler else { return try await executeSimpleQuery(sql) } @@ -110,7 +114,7 @@ extension MySQLSession { private func executeSimpleQuery(_ sql: String) async throws -> QueryResultSet { do { let result = try await client.query(sql) - return makeResultSet(from: result.rows) + return makeResultSet(from: result.rows, metadata: result.metadata) } catch { throw DatabaseError.queryError(error.localizedDescription) } @@ -165,7 +169,7 @@ extension MySQLSession { } } - private func makeResultSet(from rows: [MySQLRow]) -> QueryResultSet { + private func makeResultSet(from rows: [MySQLRow], metadata: MySQLWireQueryMetadata? = nil) -> QueryResultSet { let columns = rows.first.map { makeColumnInfo(from: $0.columnDefinitions) } ?? [] let previewRows = rows.map { row in row.values.indices.map { index in @@ -174,9 +178,18 @@ extension MySQLSession { } return QueryResultSet( - columns: columns.isEmpty ? [ColumnInfo(name: "result", dataType: "text")] : columns, + columns: columns, rows: previewRows, - totalRowCount: rows.count + totalRowCount: rows.count, + commandTag: metadata.map(commandResponse(from:)) ) } + + private func commandResponse(from metadata: MySQLWireQueryMetadata) -> String { + var segments = ["affectedRows=\(metadata.affectedRows)"] + if let lastInsertID = metadata.lastInsertID { + segments.append("lastInsertID=\(lastInsertID)") + } + return segments.joined(separator: ", ") + } } diff --git a/Echo/Sources/Core/DatabaseEngine/Dialects/MySQL/MySQLToolLocator.swift b/Echo/Sources/Core/DatabaseEngine/Dialects/MySQL/MySQLToolLocator.swift index 8aac82748..b3cfabfd3 100644 --- a/Echo/Sources/Core/DatabaseEngine/Dialects/MySQL/MySQLToolLocator.swift +++ b/Echo/Sources/Core/DatabaseEngine/Dialects/MySQL/MySQLToolLocator.swift @@ -30,7 +30,15 @@ nonisolated struct MySQLToolLocator { } private static func locateTool(name: String, customPath: String?) -> URL? { - for directory in searchDirectories(customPath: customPath) { + // When a custom path is explicitly provided, restrict the search to that + // directory only. The caller chose a specific tool location — do not fall + // through to system paths or `which`. + if let customPath, !customPath.isEmpty { + let tool = URL(fileURLWithPath: customPath).appendingPathComponent(name) + return FileManager.default.isExecutableFile(atPath: tool.path) ? tool : nil + } + + for directory in searchDirectories() { let tool = URL(fileURLWithPath: directory).appendingPathComponent(name) if FileManager.default.isExecutableFile(atPath: tool.path) { return tool @@ -58,11 +66,8 @@ nonisolated struct MySQLToolLocator { } } - private static func searchDirectories(customPath: String?) -> [String] { + private static func searchDirectories() -> [String] { var directories: [String] = [] - if let customPath, !customPath.isEmpty { - directories.append(customPath) - } let env = ProcessInfo.processInfo.environment if let envPath = env["ECHO_MYSQL_TOOL_PATH"], !envPath.isEmpty { diff --git a/Echo/Sources/Core/DatabaseEngine/Dialects/Postgres/Modules/PostgresDatabase.swift b/Echo/Sources/Core/DatabaseEngine/Dialects/Postgres/Modules/PostgresDatabase.swift index d6f754218..4c24e6367 100644 --- a/Echo/Sources/Core/DatabaseEngine/Dialects/Postgres/Modules/PostgresDatabase.swift +++ b/Echo/Sources/Core/DatabaseEngine/Dialects/Postgres/Modules/PostgresDatabase.swift @@ -113,6 +113,9 @@ final class PostgresSession: DatabaseSession { } func simpleQuery(_ sql: String, progressHandler: QueryProgressHandler?) async throws -> QueryResultSet { + if QueryStatementClassifier.isLikelyMessageOnlyStatement(sql, databaseType: .postgresql) { + return try await executeSimpleQuery(sql) + } if let progressHandler { let sanitized = sanitizeSQL(sql) return try await streamQuery(sanitizedSQL: sanitized, progressHandler: progressHandler, modeOverride: nil) @@ -122,6 +125,9 @@ final class PostgresSession: DatabaseSession { } func simpleQuery(_ sql: String, executionMode: ResultStreamingExecutionMode?, progressHandler: QueryProgressHandler?) async throws -> QueryResultSet { + if QueryStatementClassifier.isLikelyMessageOnlyStatement(sql, databaseType: .postgresql) { + return try await executeSimpleQuery(sql) + } if let progressHandler { let sanitized = sanitizeSQL(sql) return try await streamQuery(sanitizedSQL: sanitized, progressHandler: progressHandler, modeOverride: executionMode) @@ -132,7 +138,7 @@ final class PostgresSession: DatabaseSession { private func executeSimpleQuery(_ sql: String) async throws -> QueryResultSet { do { - let result = try await client.simpleQuery(sql) + let result = try await client.simpleQueryResult(sql) var columns: [ColumnInfo] = [] var rows: [[String?]] = [] @@ -140,7 +146,7 @@ final class PostgresSession: DatabaseSession { let formatter = PostgresCellFormatter() - for try await row in result { + for row in result.rows { if columns.isEmpty { let wireColumns = PostgresRowExtractor.columns(from: row) columns.reserveCapacity(wireColumns.count) @@ -169,13 +175,25 @@ final class PostgresSession: DatabaseSession { return QueryResultSet( columns: resolvedColumns, - rows: rows + rows: rows, + commandTag: rawCommandTag(from: result.metadata) ) } catch { throw normalizeError(error, contextSQL: sql) } } + private func rawCommandTag(from metadata: WireQueryMetadata) -> String { + var segments = [metadata.command] + if let oid = metadata.oid { + segments.append(String(oid)) + } + if let rows = metadata.rows { + segments.append(String(rows)) + } + return segments.joined(separator: " ") + } + func queryWithPaging(_ sql: String, limit: Int, offset: Int) async throws -> QueryResultSet { let pagedSQL = "\(sql) LIMIT \(limit) OFFSET \(offset)" return try await simpleQuery(pagedSQL) diff --git a/Echo/Sources/Core/DatabaseEngine/Dialects/Postgres/PostgresTerminalLauncher.swift b/Echo/Sources/Core/DatabaseEngine/Dialects/Postgres/PostgresTerminalLauncher.swift new file mode 100644 index 000000000..072fd07cb --- /dev/null +++ b/Echo/Sources/Core/DatabaseEngine/Dialects/Postgres/PostgresTerminalLauncher.swift @@ -0,0 +1,72 @@ +import Foundation +import OSLog + +/// Launches the native psql CLI in Terminal.app, pre-connected to a specific database. +nonisolated struct PostgresTerminalLauncher { + + private static let logger = Logger(subsystem: "com.echo.app", category: "PostgresTerminalLauncher") + + /// Opens Terminal.app with psql connected to the given database. + /// Uses `PostgresToolLocator` to find the psql binary on the user's system. + /// + /// - Parameters: + /// - host: The database server hostname. + /// - port: The database server port. + /// - username: The username for authentication. + /// - database: The database to connect to. + /// - customToolPath: Optional custom directory containing psql. + /// - Returns: `true` if Terminal was opened, `false` if psql was not found. + @discardableResult + static func openInTerminal( + host: String, + port: Int, + username: String, + database: String, + customToolPath: String? = nil + ) async -> Bool { + guard let psqlURL = PostgresToolLocator.psqlURL(customPath: customToolPath) else { + logger.warning("psql binary not found on this system") + return false + } + + let psqlPath = psqlURL.path + + // Build the psql command with connection arguments. + // Quote all arguments to handle special characters. + var parts = [shellQuote(psqlPath)] + parts.append("-h \(shellQuote(host))") + parts.append("-p \(shellQuote(String(port)))") + parts.append("-U \(shellQuote(username))") + parts.append(shellQuote(database)) + + let command = parts.joined(separator: " ") + + // Use AppleScript to open Terminal.app and run the command. + // This is the standard macOS pattern for launching CLI tools from GUI apps. + let script = """ + tell application "Terminal" + activate + do script "\(command.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))" + end tell + """ + + let appleScript = NSAppleScript(source: script) + var errorDict: NSDictionary? + appleScript?.executeAndReturnError(&errorDict) + + if let error = errorDict { + logger.error("Failed to open Terminal: \(error)") + return false + } + + logger.info("Opened psql in Terminal for \(username)@\(host):\(port)/\(database)") + return true + } + + private static func shellQuote(_ value: String) -> String { + if value.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "-" || $0 == "_" || $0 == "." || $0 == "/" }) { + return value + } + return "'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'" + } +} diff --git a/Echo/Sources/Core/DatabaseEngine/QueryResultsModels.swift b/Echo/Sources/Core/DatabaseEngine/QueryResultsModels.swift index 5c457cf4b..b8947bd13 100644 --- a/Echo/Sources/Core/DatabaseEngine/QueryResultsModels.swift +++ b/Echo/Sources/Core/DatabaseEngine/QueryResultsModels.swift @@ -15,8 +15,21 @@ public struct ServerMessage: Sendable { public let serverName: String? public let procedureName: String? public let lineNumber: Int32? + public let category: String? + public let metadata: [String: String] - public nonisolated init(kind: Kind, number: Int32, message: String, state: UInt8, severity: UInt8, serverName: String? = nil, procedureName: String? = nil, lineNumber: Int32? = nil) { + public nonisolated init( + kind: Kind, + number: Int32, + message: String, + state: UInt8, + severity: UInt8, + serverName: String? = nil, + procedureName: String? = nil, + lineNumber: Int32? = nil, + category: String? = nil, + metadata: [String: String] = [:] + ) { self.kind = kind self.number = number self.message = message @@ -25,6 +38,8 @@ public struct ServerMessage: Sendable { self.serverName = serverName self.procedureName = procedureName self.lineNumber = lineNumber + self.category = category + self.metadata = metadata } } diff --git a/Echo/Sources/Core/DatabaseEngine/QueryStatementClassifier.swift b/Echo/Sources/Core/DatabaseEngine/QueryStatementClassifier.swift new file mode 100644 index 000000000..7f676ffbe --- /dev/null +++ b/Echo/Sources/Core/DatabaseEngine/QueryStatementClassifier.swift @@ -0,0 +1,46 @@ +import Foundation + +enum QueryStatementClassifier { + static func isLikelyMessageOnlyStatement(_ sql: String, databaseType: DatabaseType) -> Bool { + let normalized = normalizedSQL(sql) + guard let keyword = leadingKeyword(in: normalized) else { return false } + + if normalized.contains(" RETURNING ") || normalized.contains(" OUTPUT ") { + return false + } + + switch keyword { + case "ALTER", "CREATE", "DROP", "RENAME", "TRUNCATE", + "INSERT", "UPDATE", "DELETE", "MERGE", + "GRANT", "REVOKE", "COMMENT", + "USE", "SET", + "BEGIN", "START", "COMMIT", "ROLLBACK", "SAVEPOINT", "RELEASE", + "VACUUM", "ANALYZE", "REINDEX", "CLUSTER", + "LISTEN", "UNLISTEN", "NOTIFY": + return true + case "CALL": + return databaseType == .postgresql + default: + return false + } + } + + private static func normalizedSQL(_ sql: String) -> String { + let trimmed = sql.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return "" } + let collapsed = trimmed.replacingOccurrences( + of: #"\s+"#, + with: " ", + options: .regularExpression + ) + return " \(collapsed.uppercased()) " + } + + private static func leadingKeyword(in normalizedSQL: String) -> String? { + normalizedSQL + .trimmingCharacters(in: .whitespacesAndNewlines) + .split(separator: " ", maxSplits: 1) + .first + .map(String.init) + } +} diff --git a/Echo/Sources/Features/Account/Domain/AppleSignInCoordinator.swift b/Echo/Sources/Features/Account/Domain/AppleSignInCoordinator.swift index b62babfd3..884bb80f9 100644 --- a/Echo/Sources/Features/Account/Domain/AppleSignInCoordinator.swift +++ b/Echo/Sources/Features/Account/Domain/AppleSignInCoordinator.swift @@ -4,18 +4,24 @@ import AuthenticationServices /// Wraps ASAuthorizationController in an async interface. final class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate { private var continuation: CheckedContinuation? + private var controller: ASAuthorizationController? /// Triggers the Sign in with Apple flow and returns the credential. func signIn() async throws -> ASAuthorizationAppleIDCredential { + guard continuation == nil else { + throw AuthError.unknown("Apple sign-in is already in progress.") + } + let provider = ASAuthorizationAppleIDProvider() let request = provider.createRequest() request.requestedScopes = [.fullName, .email] let controller = ASAuthorizationController(authorizationRequests: [request]) controller.delegate = self + self.controller = controller return try await withCheckedThrowingContinuation { continuation in - self.continuation = continuation + register(continuation) controller.performRequests() } } @@ -27,31 +33,55 @@ final class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate didCompleteWithAuthorization authorization: ASAuthorization ) { guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { - Task { @MainActor in - continuation?.resume(throwing: AuthError.unknown("Unexpected credential type.")) - continuation = nil - } + resumeFromDelegate(with: .failure(AuthError.unknown("Unexpected credential type."))) return } - Task { @MainActor in - continuation?.resume(returning: credential) - continuation = nil - } + resumeFromDelegate(with: .success(credential)) } nonisolated func authorizationController( controller: ASAuthorizationController, didCompleteWithError error: any Error ) { - let authError: AuthError + resumeFromDelegate(with: .failure(Self.mapAuthError(error))) + } + + func register(_ continuation: CheckedContinuation) { + self.continuation = continuation + } + + func complete(with result: Result) { + let continuation = self.continuation + self.continuation = nil + controller = nil + + switch result { + case .success(let credential): + continuation?.resume(returning: credential) + case .failure(let error): + continuation?.resume(throwing: error) + } + } + + nonisolated static func mapAuthError(_ error: any Error) -> AuthError { if let asError = error as? ASAuthorizationError, asError.code == .canceled { - authError = .cancelled - } else { - authError = .unknown(error.localizedDescription) + return .cancelled + } + return .unknown(error.localizedDescription) + } + + nonisolated private func resumeFromDelegate( + with result: Result + ) { + if Thread.isMainThread { + MainActor.assumeIsolated { + complete(with: result) + } + return } - Task { @MainActor in - continuation?.resume(throwing: authError) - continuation = nil + + Task(priority: .userInitiated) { @MainActor in + complete(with: result) } } } diff --git a/Echo/Sources/Features/Account/Domain/AuthState.swift b/Echo/Sources/Features/Account/Domain/AuthState.swift index 5b7aafd87..dc61782aa 100644 --- a/Echo/Sources/Features/Account/Domain/AuthState.swift +++ b/Echo/Sources/Features/Account/Domain/AuthState.swift @@ -1,5 +1,6 @@ import Foundation import Observation +import Supabase /// Observable auth state that drives the Account settings UI. /// Lives as a singleton on AppDirector and is injected into the environment. @@ -46,14 +47,26 @@ final class AuthState { do { let user = try await tokenStore.loadUser() let tokens = try await tokenStore.loadTokens() - if let user, tokens != nil { + if let user, let tokens, await ensureSupabaseSession(using: tokens) { currentUser = user + } else if user != nil || tokens != nil { + try? await tokenStore.clearAll() } } catch { // No saved session — stay signed out } } + @discardableResult + func ensureSupabaseSession() async -> Bool { + do { + guard let tokens = try await tokenStore.loadTokens() else { return false } + return await ensureSupabaseSession(using: tokens) + } catch { + return false + } + } + // MARK: - Sign In with Apple func signInWithApple(identityToken: Data, authorizationCode: Data, fullName: PersonNameComponents?) async { @@ -146,16 +159,26 @@ final class AuthState { // MARK: - Delete Account func deleteAccount() async throws { + error = nil isLoading = true defer { isLoading = false } - guard let tokens = try await tokenStore.loadTokens() else { - throw AuthError.notAuthenticated - } + do { + guard let tokens = try await tokenStore.loadTokens() else { + throw AuthError.notAuthenticated + } - try await backend.deleteAccount(accessToken: tokens.accessToken) - try await tokenStore.clearAll() - currentUser = nil + try await backend.deleteAccount(accessToken: tokens.accessToken) + try await tokenStore.clearAll() + currentUser = nil + } catch let authError as AuthError { + error = authError + throw authError + } catch { + let authError = AuthError.unknown(error.localizedDescription) + self.error = authError + throw authError + } } // MARK: - Update Profile @@ -201,4 +224,26 @@ final class AuthState { self.error = .unknown(error.localizedDescription) } } + + private func ensureSupabaseSession(using tokens: AuthTokens) async -> Bool { + guard let client = SupabaseConfig.sharedClient else { return true } + + do { + _ = try await client.auth.session + return true + } catch Supabase.AuthError.sessionMissing { + do { + guard let refreshToken = tokens.refreshToken else { return false } + _ = try await client.auth.setSession( + accessToken: tokens.accessToken, + refreshToken: refreshToken + ) + return true + } catch { + return false + } + } catch { + return true + } + } } diff --git a/Echo/Sources/Features/Account/Domain/SupabaseAuthBackend.swift b/Echo/Sources/Features/Account/Domain/SupabaseAuthBackend.swift index bad1a43ef..06ec3918d 100644 --- a/Echo/Sources/Features/Account/Domain/SupabaseAuthBackend.swift +++ b/Echo/Sources/Features/Account/Domain/SupabaseAuthBackend.swift @@ -120,9 +120,20 @@ nonisolated struct SupabaseAuthBackend: AuthBackend { // MARK: - Account Management func deleteAccount(accessToken: String) async throws { - // Supabase doesn't have a client-side delete — requires a server function or admin API - // For now, sign out. Full deletion requires a server-side Edge Function. - try await client.auth.signOut() + do { + let response: DeleteAccountResponse = try await client.functions.invoke( + "delete-account", + options: FunctionInvokeOptions( + body: DeleteAccountRequest() + ) + ) + + if response.success != true { + throw AuthError.unknown(response.error ?? "Delete account failed.") + } + } catch { + throw mapDeleteAccountError(error) + } } func linkAccount(method: AuthMethod, accessToken: String, payload: Data) async throws -> AuthUser { @@ -195,3 +206,61 @@ nonisolated struct SupabaseAuthBackend: AuthBackend { private extension Auth.User { // Supabase SDK uses AnyJSON for metadata } + +private extension SupabaseAuthBackend { + struct DeleteAccountRequest: Encodable {} + + struct DeleteAccountResponse: Decodable { + let success: Bool + let error: String? + } + + func mapDeleteAccountError(_ error: any Error) -> AuthError { + if let functionsError = error as? FunctionsError { + switch functionsError { + case .relayError: + return .unknown( + "Delete Account failed because the Supabase Edge Function relay could not reach 'delete-account'." + ) + case .httpError(let code, let data): + let body = String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) + + if code == 404 || code == 503 { + return .unknown( + "Delete Account requires a deployed Supabase Edge Function named 'delete-account'." + ) + } + + if let body, !body.isEmpty, let serverMessage = extractDeleteAccountMessage(from: body) { + return .unknown(serverMessage) + } + + return .unknown("Delete Account failed with HTTP \(code).") + } + } + + let message = String(describing: error) + if message.localizedCaseInsensitiveContains("delete-account") { + return .unknown( + "Delete Account requires a deployed Supabase Edge Function named 'delete-account'." + ) + } + return .unknown(message) + } + + func extractDeleteAccountMessage(from body: String) -> String? { + if let data = body.data(using: .utf8), + let payload = try? JSONDecoder().decode(DeleteAccountResponse.self, from: data), + let error = payload.error, + !error.isEmpty { + return error + } + + if body.isEmpty { + return nil + } + + return body + } +} diff --git a/Echo/Sources/Features/Account/Sync/E2E/E2EEnrollmentManager.swift b/Echo/Sources/Features/Account/Sync/E2E/E2EEnrollmentManager.swift index ce990db39..bf0c8dde5 100644 --- a/Echo/Sources/Features/Account/Sync/E2E/E2EEnrollmentManager.swift +++ b/Echo/Sources/Features/Account/Sync/E2E/E2EEnrollmentManager.swift @@ -86,6 +86,11 @@ final class E2EEnrollmentManager { error = nil + await checkEnrollmentStatus() + if isEnrolled { + throw E2EError.enrollmentFailed("Credential sync is already set up for this account. Enter your existing master password to unlock it, or use recovery if you forgot it.") + } + // 1. Generate salts let masterSalt = crypto.generateSalt() let recoverySalt = crypto.generateSalt() @@ -130,7 +135,8 @@ final class E2EEnrollmentManager { let userID = try await client.auth.session.user.id // Update profile - struct ProfileUpdate: Encodable { + struct ProfileEnrollmentRow: Encodable { + let id: UUID let e2e_enrolled: Bool let e2e_salt: String // base64 let e2e_wrapped_master_key: String // base64 @@ -138,14 +144,14 @@ final class E2EEnrollmentManager { let e2e_recovery_salt: String // base64 } try await client.from("profiles") - .update(ProfileUpdate( + .upsert(ProfileEnrollmentRow( + id: userID, e2e_enrolled: true, e2e_salt: masterSalt.base64EncodedString(), e2e_wrapped_master_key: wrappedMasterKey.base64EncodedString(), e2e_recovery_key_hash: recoveryHashHex, e2e_recovery_salt: recoverySalt.base64EncodedString() - )) - .eq("id", value: userID) + ), onConflict: "id") .execute() // Upload wrapped project keys @@ -165,7 +171,7 @@ final class E2EEnrollmentManager { project_id: serverProjectID, wrapped_key: wpk.wrappedKey.base64EncodedString(), nonce: Data().base64EncodedString() - )) + ), onConflict: "user_id,project_id") .execute() } @@ -288,7 +294,7 @@ final class E2EEnrollmentManager { project_id: serverProjectID, wrapped_key: rewrapped.base64EncodedString(), nonce: Data().base64EncodedString() - )) + ), onConflict: "user_id,project_id") .execute() } @@ -296,16 +302,17 @@ final class E2EEnrollmentManager { let newWrappedMasterKey = try crypto.wrapKey(newMasterKey, with: recoveryKEK) // 9. Update server - struct ProfileUpdate: Encodable { + struct ProfileRecoveryRow: Encodable { + let id: UUID let e2e_salt: String let e2e_wrapped_master_key: String } try await client.from("profiles") - .update(ProfileUpdate( + .upsert(ProfileRecoveryRow( + id: userID, e2e_salt: newSalt.base64EncodedString(), e2e_wrapped_master_key: newWrappedMasterKey.base64EncodedString() - )) - .eq("id", value: userID) + ), onConflict: "id") .execute() // 10. Store new Master Key locally diff --git a/Echo/Sources/Features/Account/Sync/SyncAdapter.swift b/Echo/Sources/Features/Account/Sync/SyncAdapter.swift index bcf7bca72..4e5184a43 100644 --- a/Echo/Sources/Features/Account/Sync/SyncAdapter.swift +++ b/Echo/Sources/Features/Account/Sync/SyncAdapter.swift @@ -1,3 +1,4 @@ +import CryptoKit import Foundation /// Converts between Echo domain models and SyncDocument format. @@ -284,19 +285,15 @@ struct SyncAdapter: Sendable { /// Deterministic ID for a project's settings document. func settingsDocumentID(for projectID: UUID) -> UUID { - // Use a namespace UUID derived from the project ID so it's stable let input = "settings:\(projectID.uuidString)" - let hash = Array(input.utf8).withUnsafeBufferPointer { buffer -> [UInt8] in - var result = [UInt8](repeating: 0, count: 16) - for (i, byte) in buffer.enumerated() { - result[i % 16] ^= byte - } - return result - } - return UUID(uuid: (hash[0], hash[1], hash[2], hash[3], - hash[4], hash[5], hash[6], hash[7], - hash[8], hash[9], hash[10], hash[11], - hash[12], hash[13], hash[14], hash[15])) + let digest = SHA256.hash(data: Data(input.utf8)) + var bytes = Array(digest.prefix(16)) + bytes[6] = (bytes[6] & 0x0F) | 0x50 + bytes[8] = (bytes[8] & 0x3F) | 0x80 + return UUID(uuid: (bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + bytes[8], bytes[9], bytes[10], bytes[11], + bytes[12], bytes[13], bytes[14], bytes[15])) } // MARK: - Field Helpers diff --git a/Echo/Sources/Features/Account/Sync/SyncCheckpointStore.swift b/Echo/Sources/Features/Account/Sync/SyncCheckpointStore.swift index 36e24ce12..bd59cb2de 100644 --- a/Echo/Sources/Features/Account/Sync/SyncCheckpointStore.swift +++ b/Echo/Sources/Features/Account/Sync/SyncCheckpointStore.swift @@ -25,6 +25,10 @@ actor SyncCheckpointStore { checkpoints[projectID]?.checkpoint ?? 0 } + func hasCheckpoint(for projectID: UUID) -> Bool { + checkpoints[projectID] != nil + } + func update(projectID: UUID, checkpoint: UInt64) throws { checkpoints[projectID] = SyncCheckpoint( projectID: projectID, diff --git a/Echo/Sources/Features/Account/Sync/SyncClient.swift b/Echo/Sources/Features/Account/Sync/SyncClient.swift index 2fbec439b..b06b910c6 100644 --- a/Echo/Sources/Features/Account/Sync/SyncClient.swift +++ b/Echo/Sources/Features/Account/Sync/SyncClient.swift @@ -1,5 +1,6 @@ import CryptoKit import Foundation +import os.log import Supabase /// HTTP client for the Supabase sync RPC endpoints. @@ -9,6 +10,7 @@ import Supabase /// JWT injection and token refresh automatically. nonisolated final class SyncClient: Sendable { private let client: SupabaseClient + private let logger = Logger(subsystem: "dev.echodb.echo", category: "sync-client") init?() { guard let client = SupabaseConfig.sharedClient else { return nil } @@ -60,15 +62,42 @@ nonisolated final class SyncClient: Sendable { // MARK: - Push /// Push local changes to the server. - func push(changes: [SyncDocument]) async throws -> SyncPushResponse { + func push(changes: [SyncDocument], projectID: UUID) async throws -> SyncPushResponse { let params = SyncPushParams(p_changes: changes) - let response: SyncPushResponse = try await client.rpc( - "sync_push", - params: params - ).execute().value + do { + let response: SyncPushResponse = try await client.rpc( + "sync_push", + params: params + ).execute().value - return response + return response + } catch let error as PostgrestError where shouldRecoverFromDuplicateDocument(error) { + let removedCount = try await removeStaleDocumentsConflicting(with: changes, currentProjectID: projectID) + guard removedCount > 0 else { throw error } + + logger.warning("Removed \(removedCount) stale sync documents after duplicate-key conflict; retrying push") + + let response: SyncPushResponse = try await client.rpc( + "sync_push", + params: params + ).execute().value + + return response + } + } + + // MARK: - Pre-flight Check + + /// Check how many sync documents exist on the server for a given project. + /// Used to determine whether a merge strategy prompt is needed. + func cloudDocumentCount(projectID: UUID) async throws -> Int { + let response = try await client.from("sync_documents") + .select("*", head: true, count: .exact) + .eq("project_id", value: projectID) + .eq("is_deleted", value: false) + .execute() + return response.count ?? 0 } // MARK: - Project Registration @@ -93,6 +122,41 @@ nonisolated final class SyncClient: Sendable { )) .execute() } + + private func shouldRecoverFromDuplicateDocument(_ error: PostgrestError) -> Bool { + error.message.contains("sync_documents_pkey") + || (error.detail?.contains("sync_documents_pkey") ?? false) + } + + private func removeStaleDocumentsConflicting(with changes: [SyncDocument], currentProjectID: UUID) async throws -> Int { + let ids = Array(Set(changes.map(\.id))) + guard !ids.isEmpty else { return 0 } + + struct ExistingDocumentRow: Decodable { + let id: UUID + let project_id: UUID + let is_deleted: Bool + } + + let existingRows: [ExistingDocumentRow] = try await client.from("sync_documents") + .select("id, project_id, is_deleted") + .in("id", values: ids) + .execute() + .value + + let staleIDs = Array(Set(existingRows.lazy + .filter { $0.project_id != currentProjectID || $0.is_deleted } + .map(\.id))) + + guard !staleIDs.isEmpty else { return 0 } + + _ = try await client.from("sync_documents") + .delete(returning: .minimal) + .in("id", values: staleIDs) + .execute() + + return staleIDs.count + } } // MARK: - RPC Parameter Types diff --git a/Echo/Sources/Features/Account/Sync/SyncEngine+InitialSync.swift b/Echo/Sources/Features/Account/Sync/SyncEngine+InitialSync.swift new file mode 100644 index 000000000..9b5ffc0ac --- /dev/null +++ b/Echo/Sources/Features/Account/Sync/SyncEngine+InitialSync.swift @@ -0,0 +1,27 @@ +import Foundation + +extension SyncEngine { + func nextStartupRequirement() async throws -> (project: Project, summary: SyncDataSummary, action: SyncStartupAction)? { + guard let projectStore else { return nil } + + for project in projectStore.projects where project.isSyncEnabled { + let hasCheckpoint = await checkpointStore.hasCheckpoint(for: project.id) + let summary = try await checkSyncDataSummary(for: project) + let action = summary.startupAction(hasCheckpoint: hasCheckpoint) + if action != .none { + return (project, summary, action) + } + } + + return nil + } + + func hasPendingMergeDecision() async -> Bool { + do { + guard let requirement = try await nextStartupRequirement() else { return false } + return requirement.action == .promptForMerge + } catch { + return false + } + } +} diff --git a/Echo/Sources/Features/Account/Sync/SyncEngine.swift b/Echo/Sources/Features/Account/Sync/SyncEngine.swift index 8b654746d..a796175e2 100644 --- a/Echo/Sources/Features/Account/Sync/SyncEngine.swift +++ b/Echo/Sources/Features/Account/Sync/SyncEngine.swift @@ -1,6 +1,7 @@ import Foundation import Observation import os.log +import Supabase /// Orchestrates cloud sync between local Echo stores and the Supabase backend. /// @@ -27,7 +28,7 @@ final class SyncEngine { @ObservationIgnored private let syncClient: SyncClient @ObservationIgnored private let adapter: SyncAdapter @ObservationIgnored private let merger: SyncMerger - @ObservationIgnored private let checkpointStore: SyncCheckpointStore + @ObservationIgnored let checkpointStore: SyncCheckpointStore @ObservationIgnored private let dirtyTracker: SyncDirtyTracker @ObservationIgnored private let fieldEncryptor = E2EFieldEncryptor() @@ -116,6 +117,12 @@ final class SyncEngine { return } + if await hasPendingMergeDecision() { + logger.info("Sync is waiting for a merge decision before the initial sync can continue") + status = .idle + return + } + isSyncing = true status = .syncing @@ -147,6 +154,9 @@ final class SyncEngine { } catch is CancellationError { logger.debug("Sync cancelled") status = .idle + } catch Supabase.AuthError.sessionMissing { + logger.info("Skipping sync because the auth session is not ready yet") + status = .idle } catch { logger.error("Sync failed: \(error.localizedDescription)") status = .error(error.localizedDescription) @@ -231,7 +241,7 @@ final class SyncEngine { } else { let existing = connectionStore.connections.first { $0.id == doc.id } var connection = try adapter.applyToConnection(doc, existing: existing) - applyEncryptedCredentials(from: doc, projectID: project.id, keychainID: &connection.keychainIdentifier) + applyEncryptedCredentials(from: doc, projectID: project.id, keychainID: &connection.keychainIdentifier, displayName: connection.connectionName) try await connectionStore.updateConnection(connection) } @@ -254,7 +264,7 @@ final class SyncEngine { } else { let existing = connectionStore.identities.first { $0.id == doc.id } var identity = try adapter.applyToIdentity(doc, existing: existing) - applyEncryptedCredentials(from: doc, projectID: project.id, keychainID: &identity.keychainIdentifier) + applyEncryptedCredentials(from: doc, projectID: project.id, keychainID: &identity.keychainIdentifier, displayName: identity.name) try await connectionStore.updateIdentity(identity) } @@ -372,7 +382,7 @@ final class SyncEngine { guard !documents.isEmpty else { return } - let response = try await syncClient.push(changes: documents) + let response = try await syncClient.push(changes: documents, projectID: serverProjectID) logger.info("Pushed \(response.accepted) documents") if !response.conflicts.isEmpty { @@ -386,7 +396,8 @@ final class SyncEngine { /// Add encrypted password field to a sync document if E2E is active. private func addEncryptedCredentials(to doc: inout SyncDocument, keychainID: String?, projectID: UUID, hlc: UInt64) throws { - guard let keyStore = e2eKeyStore, keyStore.isUnlocked, + guard SyncPreferences.isCredentialSyncEnabled, + let keyStore = e2eKeyStore, keyStore.isUnlocked, let projectKey = keyStore.projectKey(for: projectID), let keychainID, !keychainID.isEmpty else { return } @@ -406,13 +417,16 @@ final class SyncEngine { // MARK: - E2E Credential Decryption (Pull) /// Decrypt and store credentials from a pulled sync document. - private func applyEncryptedCredentials(from doc: SyncDocument, projectID: UUID, keychainID: inout String?) { - guard let keyStore = e2eKeyStore, keyStore.isUnlocked, + /// If the local Keychain already has a different password, a conflict is recorded + /// instead of silently overwriting. + private func applyEncryptedCredentials(from doc: SyncDocument, projectID: UUID, keychainID: inout String?, displayName: String = "") { + guard SyncPreferences.isCredentialSyncEnabled, + let keyStore = e2eKeyStore, keyStore.isUnlocked, let projectKey = keyStore.projectKey(for: projectID), let encField = doc.fields["encryptedPassword"], encField.isEncrypted else { return } do { - let password = try fieldEncryptor.decryptField( + let cloudPassword = try fieldEncryptor.decryptField( field: encField, key: projectKey, collection: doc.collection, @@ -427,7 +441,26 @@ final class SyncEngine { } let vault = KeychainVault() - try vault.setPassword(password, account: keychainID!) + + // Check for conflict: local Keychain has a different password + if let localPassword = try? vault.getPassword(account: keychainID!), + !localPassword.isEmpty, + localPassword != cloudPassword { + // Record the conflict for user resolution + let name = displayName.isEmpty ? doc.id.uuidString : displayName + pendingCredentialConflicts.append(CredentialConflict( + id: doc.id, + collection: doc.collection, + displayName: name, + localPassword: localPassword, + cloudPassword: cloudPassword + )) + logger.info("Credential conflict detected for \(doc.id) — deferring to user") + return + } + + // No conflict — apply cloud password + try vault.setPassword(cloudPassword, account: keychainID!) } catch { logger.error("Failed to decrypt credential for \(doc.id): \(error.localizedDescription)") } @@ -435,13 +468,70 @@ final class SyncEngine { // MARK: - Initial Upload + /// Check whether the server already has data for this project and build a summary. + /// The caller uses this to decide whether to show a merge strategy prompt. + func checkSyncDataSummary(for project: Project) async throws -> SyncDataSummary { + guard let connectionStore else { + return SyncDataSummary(localConnections: 0, localIdentities: 0, localFolders: 0, localBookmarks: 0, cloudDocuments: 0) + } + + let userID = try await syncClient.currentUserID() + let serverID = syncClient.serverProjectID(localID: project.id, userID: userID) + let cloudCount = try await syncClient.cloudDocumentCount(projectID: serverID) + + return SyncDataSummary( + localConnections: connectionStore.connections.filter { $0.projectID == project.id }.count, + localIdentities: connectionStore.identities.filter { $0.projectID == project.id }.count, + localFolders: connectionStore.folders.filter { $0.projectID == project.id }.count, + localBookmarks: project.bookmarks.count, + cloudDocuments: cloudCount + ) + } + /// Upload all local data for a project to the server for the first time. /// Called when a user enables sync on an existing project. - func performInitialUpload(for project: Project) async throws { - guard let connectionStore, let projectStore else { return } + /// + /// - Parameter strategy: How to handle existing data on both sides. + /// - `.uploadLocal`: Push local data to cloud (original behavior). + /// - `.useCloud`: Pull cloud data and replace local state. Local-only items are deleted. + /// - `.merge`: Push local, then pull cloud — LWW resolves conflicts. + func performInitialUpload(for project: Project, strategy: SyncMergeStrategy = .merge) async throws { + guard let projectStore else { return } + + // Register project on server first (using per-user server ID) + let userID = try await syncClient.currentUserID() + let serverID = syncClient.serverProjectID(localID: project.id, userID: userID) + let sortOrder = projectStore.projects.firstIndex(where: { $0.id == project.id }) ?? 0 + try await syncClient.upsertProject(serverID: serverID, userID: userID, name: project.name, sortOrder: sortOrder) + + switch strategy { + case .uploadLocal: + try await uploadLocalData(for: project) + try await pullChanges(for: project, serverProjectID: serverID) + logger.info("Initial upload (uploadLocal) complete for '\(project.name)'") + + case .useCloud: + // Delete local data for this project, then pull everything from cloud + try await deleteLocalProjectData(for: project) + try await pullChanges(for: project, serverProjectID: serverID) + logger.info("Initial sync (useCloud) complete for '\(project.name)'") + + case .merge: + // Upload local first, then pull cloud — LWW resolves conflicts at field level + try await uploadLocalData(for: project) + try await pullChanges(for: project, serverProjectID: serverID) + logger.info("Initial sync (merge) complete for '\(project.name)'") + } + } + + /// Push all local data for a project to the server. + private func uploadLocalData(for project: Project) async throws { + guard let connectionStore else { return } var documents: [SyncDocument] = [] let hlc = clock.now() + let userID = try await syncClient.currentUserID() + let serverProjectID = syncClient.serverProjectID(localID: project.id, userID: userID) // Project itself documents.append(try adapter.toSyncDocument(project, hlc: hlc)) @@ -449,7 +539,9 @@ final class SyncEngine { // Connections belonging to this project let projectConnections = connectionStore.connections.filter { $0.projectID == project.id } for conn in projectConnections { - documents.append(try adapter.toSyncDocument(conn, hlc: hlc)) + var doc = try adapter.toSyncDocument(conn, hlc: hlc) + try addEncryptedCredentials(to: &doc, keychainID: conn.keychainIdentifier, projectID: project.id, hlc: hlc) + documents.append(doc) } // Folders belonging to this project @@ -461,7 +553,9 @@ final class SyncEngine { // Identities belonging to this project let projectIdentities = connectionStore.identities.filter { $0.projectID == project.id } for identity in projectIdentities { - documents.append(try adapter.toSyncDocument(identity, hlc: hlc)) + var doc = try adapter.toSyncDocument(identity, hlc: hlc) + try addEncryptedCredentials(to: &doc, keychainID: identity.keychainIdentifier, projectID: project.id, hlc: hlc) + documents.append(doc) } // Bookmarks @@ -476,21 +570,79 @@ final class SyncEngine { guard !documents.isEmpty else { return } - // Register project on server first (using per-user server ID) - let userID = try await syncClient.currentUserID() - let serverID = syncClient.serverProjectID(localID: project.id, userID: userID) - let sortOrder = projectStore.projects.firstIndex(where: { $0.id == project.id }) ?? 0 - try await syncClient.upsertProject(serverID: serverID, userID: userID, name: project.name, sortOrder: sortOrder) - // Push in batches of 100 let batchSize = 100 for batchStart in stride(from: 0, to: documents.count, by: batchSize) { let batchEnd = min(batchStart + batchSize, documents.count) let batch = Array(documents[batchStart..? private var idleTimerTask: Task? + private var triggerTask: Task? private var isRunning = false // MARK: - Init @@ -47,6 +48,8 @@ final class SyncScheduler { /// Stop the scheduler. Called on sign-out. func stop() { isRunning = false + triggerTask?.cancel() + triggerTask = nil debounceTask?.cancel() debounceTask = nil idleTimerTask?.cancel() @@ -60,13 +63,12 @@ final class SyncScheduler { func scheduleSync() { guard isRunning else { return } debounceTask?.cancel() - let interval = changeDebounceInterval - let engineRef = syncEngine - debounceTask = Task.detached { + debounceTask = Task { [weak self] in + guard let self else { return } do { - try await Task.sleep(for: .seconds(interval)) - guard !Task.isCancelled else { return } - await engineRef?.syncNow() + try await Task.sleep(for: .seconds(self.changeDebounceInterval)) + guard !Task.isCancelled, self.isRunning else { return } + await self.syncEngine?.syncNow() } catch { // Task was cancelled — another change came in } @@ -83,22 +85,22 @@ final class SyncScheduler { // MARK: - Private private func triggerSync() { - let engineRef = syncEngine - Task.detached { - await engineRef?.syncNow() + triggerTask?.cancel() + triggerTask = Task { [weak self] in + guard let self, self.isRunning else { return } + await self.syncEngine?.syncNow() } } private func startIdleTimer() { idleTimerTask?.cancel() - let interval = idleSyncInterval - let engineRef = syncEngine - idleTimerTask = Task.detached { + idleTimerTask = Task { [weak self] in + guard let self else { return } while !Task.isCancelled { do { - try await Task.sleep(for: .seconds(interval)) - guard !Task.isCancelled else { break } - await engineRef?.syncNow() + try await Task.sleep(for: .seconds(self.idleSyncInterval)) + guard !Task.isCancelled, self.isRunning else { break } + await self.syncEngine?.syncNow() } catch { break } diff --git a/Echo/Sources/Features/Account/Sync/SyncTypes.swift b/Echo/Sources/Features/Account/Sync/SyncTypes.swift index 70d38aeab..1ee7735ad 100644 --- a/Echo/Sources/Features/Account/Sync/SyncTypes.swift +++ b/Echo/Sources/Features/Account/Sync/SyncTypes.swift @@ -28,7 +28,7 @@ enum SyncCollection: String, Codable, Sendable, CaseIterable { switch self { case .connections: "Server addresses, ports, and connection options" case .folders: "Folder structure for organizing connections" - case .identities: "Saved login names (passwords stay in Keychain)" + case .identities: "Saved login names and identity metadata" case .projects: "Project names and configuration" case .settings: "App preferences and editor settings" case .bookmarks: "Saved queries and snippets" @@ -88,6 +88,81 @@ enum SyncPreferences { } return Set(stored.compactMap { SyncCollection(rawValue: $0) }).union(alwaysEnabled) } + + // MARK: - Credential Sync Toggle + + private static let credentialSyncKey = "sync.credentialSyncEnabled" + + static var isCredentialSyncEnabled: Bool { + // Default: true (enabled once E2E is enrolled) + UserDefaults.standard.object(forKey: credentialSyncKey) as? Bool ?? true + } + + static func setCredentialSyncEnabled(_ enabled: Bool) { + UserDefaults.standard.set(enabled, forKey: credentialSyncKey) + } +} + +// MARK: - Sync Merge Strategy + +/// How to handle the first sync when both local and cloud data exist for a project. +enum SyncMergeStrategy: Sendable { + /// Combine local and cloud data. Conflicts resolved by most recent change (LWW). + case merge + /// Replace local data with what's in the cloud. Local-only items are deleted. + case useCloud + /// Push local data to cloud, overwriting cloud versions where they conflict. + case uploadLocal +} + +enum SyncStartupAction: Sendable, Equatable { + case none + case promptForMerge + case pullCloud + case uploadLocal +} + +/// Summary of data counts for a project, used to inform the merge strategy prompt. +struct SyncDataSummary: Sendable { + let localConnections: Int + let localIdentities: Int + let localFolders: Int + let localBookmarks: Int + let cloudDocuments: Int + + var hasLocalData: Bool { + localConnections + localIdentities + localFolders + localBookmarks > 0 + } + + var hasCloudData: Bool { + cloudDocuments > 0 + } + + var needsMergeDecision: Bool { + hasLocalData && hasCloudData + } + + var localTotal: Int { + localConnections + localIdentities + localFolders + localBookmarks + } + + func startupAction(hasCheckpoint: Bool) -> SyncStartupAction { + guard !hasCheckpoint else { return .none } + if needsMergeDecision { return .promptForMerge } + if hasCloudData { return .pullCloud } + if hasLocalData { return .uploadLocal } + return .none + } +} + +/// A credential conflict detected during pull — local Keychain has a different +/// password than what the cloud has for the same connection/identity. +struct CredentialConflict: Identifiable, Sendable, Equatable { + let id: UUID + let collection: SyncCollection + let displayName: String + let localPassword: String + let cloudPassword: String } // MARK: - Sync Field diff --git a/Echo/Sources/Features/Account/Views/CredentialConflictSheet.swift b/Echo/Sources/Features/Account/Views/CredentialConflictSheet.swift new file mode 100644 index 000000000..f00980af4 --- /dev/null +++ b/Echo/Sources/Features/Account/Views/CredentialConflictSheet.swift @@ -0,0 +1,83 @@ +import SwiftUI + +/// Presented when pulling credentials from the cloud detects that the local Keychain +/// has different passwords for the same connections/identities. +struct CredentialConflictSheet: View { + let conflicts: [CredentialConflict] + let onResolve: (Bool) -> Void // true = use cloud, false = keep local + + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 0) { + Form { + Section { + VStack(alignment: .leading, spacing: SpacingTokens.sm) { + Label("Credential Conflict", systemImage: "exclamationmark.lock") + .font(TypographyTokens.headline) + + Text("\(conflicts.count) credential\(conflicts.count == 1 ? "" : "s") differ between this device and the cloud. Which version would you like to keep?") + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Text.secondary) + } + .padding(.bottom, SpacingTokens.xs) + + // List affected items + ForEach(conflicts) { conflict in + Label { + Text(conflict.displayName) + } icon: { + Image(systemName: conflict.collection == .connections ? "externaldrive" : "person.crop.circle") + .foregroundStyle(ColorTokens.Text.secondary) + } + } + } + + Section { + Button { + onResolve(true) + dismiss() + } label: { + Label { + VStack(alignment: .leading, spacing: 2) { + Text("Use Cloud Passwords") + Text("Replace local passwords with the cloud versions. Use this if your cloud data is more up to date.") + .font(TypographyTokens.detail) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } icon: { + Image(systemName: "icloud.and.arrow.down") + .foregroundStyle(ColorTokens.Text.secondary) + .frame(width: 20) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + + Button { + onResolve(false) + dismiss() + } label: { + Label { + VStack(alignment: .leading, spacing: 2) { + Text("Keep Local Passwords") + Text("Keep the passwords already on this device. They will be uploaded to the cloud on the next sync.") + .font(TypographyTokens.detail) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } icon: { + Image(systemName: "laptopcomputer") + .foregroundStyle(ColorTokens.Text.secondary) + .frame(width: 20) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + .formStyle(.grouped) + .scrollContentBackground(.hidden) + } + .frame(width: 420, height: min(CGFloat(300 + conflicts.count * 30), 500)) + } +} diff --git a/Echo/Sources/Features/Account/Views/E2EEnrollmentView.swift b/Echo/Sources/Features/Account/Views/E2EEnrollmentView.swift index dd1537a53..9cb3c39a2 100644 --- a/Echo/Sources/Features/Account/Views/E2EEnrollmentView.swift +++ b/Echo/Sources/Features/Account/Views/E2EEnrollmentView.swift @@ -5,6 +5,7 @@ import UniformTypeIdentifiers /// Step 1: Master password → Step 2: Recovery key display → Step 3: Confirmation. struct E2EEnrollmentView: View { @Bindable var enrollmentManager: E2EEnrollmentManager + let onComplete: () async -> Void @Environment(\.dismiss) private var dismiss @State private var step: EnrollmentStep = .password @@ -32,58 +33,60 @@ struct E2EEnrollmentView: View { doneStep } } - .frame(width: 460, height: 400) + .frame(width: 520, height: sheetHeight) } // MARK: - Step 1: Master Password private var passwordStep: some View { - Form { - Section { - VStack(alignment: .leading, spacing: SpacingTokens.sm) { - Label("Create Master Password", systemImage: "lock.shield") - .font(TypographyTokens.headline) - - Text("This password encrypts your database credentials before they leave this device. Echo cannot reset it.") - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Text.secondary) - } - .padding(.bottom, SpacingTokens.xs) - - SecureField("", text: $password, prompt: Text("Master password")) - - SecureField("", text: $confirmPassword, prompt: Text("Confirm master password")) - - if let errorMessage { - Text(errorMessage) - .font(TypographyTokens.detail) - .foregroundStyle(ColorTokens.Status.error) - } - } - - Section { - HStack { - Button("Cancel") { dismiss() } - - Spacer() + SheetLayout( + title: "Create Master Password", + icon: "lock.shield", + subtitle: "This password encrypts your database credentials before they leave this device. Echo cannot reset it.", + primaryAction: "Continue", + canSubmit: canContinue, + isSubmitting: isProcessing, + errorMessage: errorMessage, + onSubmit: { await beginEnrollment() }, + onCancel: { dismiss() } + ) { + Form { + Section { + PropertyRow(title: "Master Password") { + SecureField("", text: $password, prompt: Text("At least 8 characters")) + .textFieldStyle(.plain) + .multilineTextAlignment(.trailing) + } - Button("Continue") { - Task { await beginEnrollment() } + PropertyRow(title: "Confirm Password") { + SecureField("", text: $confirmPassword, prompt: Text("Re-enter password")) + .textFieldStyle(.plain) + .multilineTextAlignment(.trailing) } - .buttonStyle(.bordered) - .keyboardShortcut(.defaultAction) - .disabled(!canContinue || isProcessing) + } footer: { + Text("Use a password you can remember. If you forget it, only your recovery key can restore access.") } } + .formStyle(.grouped) + .scrollContentBackground(.hidden) } - .formStyle(.grouped) - .scrollContentBackground(.hidden) } private var canContinue: Bool { password.count >= 8 && password == confirmPassword } + private var sheetHeight: CGFloat { + switch step { + case .password: + 320 + case .recoveryKey: + 520 + case .done: + 320 + } + } + private func saveRecoveryKeyToFile() { let panel = NSSavePanel() panel.nameFieldStringValue = "Echo Recovery Key.txt" @@ -115,6 +118,7 @@ struct E2EEnrollmentView: View { do { recoveryWords = try await enrollmentManager.enroll(password: password) + SyncPreferences.setCredentialSyncEnabled(true) step = .recoveryKey } catch { errorMessage = error.localizedDescription @@ -124,69 +128,84 @@ struct E2EEnrollmentView: View { // MARK: - Step 2: Recovery Key private var recoveryKeyStep: some View { - Form { - Section { - VStack(alignment: .leading, spacing: SpacingTokens.sm) { - Label("Save Your Recovery Key", systemImage: "key") - .font(TypographyTokens.headline) - - Text("If you forget your master password, this is the **only way** to recover your encrypted credentials. Write it down and store it somewhere safe.") - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Text.secondary) + SheetLayoutCustomFooter(title: "Save Your Recovery Key") { + VStack(alignment: .leading, spacing: SpacingTokens.md) { + HStack(spacing: SpacingTokens.sm) { + Image(systemName: "key") + .font(.system(size: 18)) + .foregroundStyle(.white) + .frame(width: 36, height: 36) + .background(Color.accentColor, in: .rect(cornerRadius: ShapeTokens.CornerRadius.medium)) + + VStack(alignment: .leading, spacing: SpacingTokens.xxs2) { + Text("Save Your Recovery Key") + .font(TypographyTokens.prominent.weight(.semibold)) + + Text("If you forget your master password, this is the only way to recover your encrypted credentials. Write it down and store it somewhere safe.") + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Text.secondary) + } + + Spacer() } + .padding(.horizontal, SpacingTokens.lg) + .padding(.top, SpacingTokens.md) .padding(.bottom, SpacingTokens.xs) - // 3 columns × 8 rows grid - Grid(alignment: .leading, horizontalSpacing: SpacingTokens.lg, verticalSpacing: SpacingTokens.xs) { - ForEach(0..<8, id: \.self) { row in - GridRow { - ForEach(0..<3, id: \.self) { col in - let idx = row * 3 + col - HStack(spacing: 4) { - Text("\(idx + 1).") - .font(TypographyTokens.detailMono) - .foregroundStyle(ColorTokens.Text.tertiary) - .frame(width: 22, alignment: .trailing) - Text(recoveryWords[idx]) - .font(TypographyTokens.codeMedium) + Divider() + + VStack(alignment: .leading, spacing: SpacingTokens.md) { + Grid(alignment: .leading, horizontalSpacing: SpacingTokens.lg, verticalSpacing: SpacingTokens.xs) { + ForEach(0..<8, id: \.self) { row in + GridRow { + ForEach(0..<3, id: \.self) { col in + let idx = row * 3 + col + HStack(spacing: 4) { + Text("\(idx + 1).") + .font(TypographyTokens.detailMono) + .foregroundStyle(ColorTokens.Text.tertiary) + .frame(width: 22, alignment: .trailing) + Text(recoveryWords[idx]) + .font(TypographyTokens.codeMedium) + } + .frame(minWidth: 118, alignment: .leading) } - .frame(minWidth: 110, alignment: .leading) } } } - } - .padding(SpacingTokens.md) - .background(ColorTokens.Background.secondary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8)) + .padding(SpacingTokens.md) + .background(ColorTokens.Background.secondary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8)) - HStack(spacing: SpacingTokens.sm) { - Button("Copy to Clipboard") { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(recoveryWords.joined(separator: " "), forType: .string) - } + HStack(spacing: SpacingTokens.sm) { + Button("Copy to Clipboard") { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(recoveryWords.joined(separator: " "), forType: .string) + } - Button("Save to File…") { - saveRecoveryKeyToFile() + Button("Save to File…") { + saveRecoveryKeyToFile() + } } + .font(TypographyTokens.formDescription) + + Toggle("I have saved this recovery key in a safe place", isOn: $savedRecoveryKey) + .toggleStyle(.checkbox) + .font(TypographyTokens.formLabel) } - .font(TypographyTokens.formDescription) + .padding(.horizontal, SpacingTokens.lg) + .padding(.bottom, SpacingTokens.lg) } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } footer: { + Button("Back") { step = .password } - Section { - Toggle("I have saved this recovery key in a safe place", isOn: $savedRecoveryKey) - .toggleStyle(.checkbox) + Spacer() - HStack { - Button("Back") { step = .password } - Spacer() - Button("Finish") { step = .done } - .buttonStyle(.bordered) - .keyboardShortcut(.defaultAction) - .disabled(!savedRecoveryKey) - } - } + Button("Finish") { step = .done } + .buttonStyle(.bordered) + .keyboardShortcut(.defaultAction) + .disabled(!savedRecoveryKey) } - .formStyle(.grouped) - .scrollContentBackground(.hidden) } // MARK: - Step 3: Done @@ -210,7 +229,10 @@ struct E2EEnrollmentView: View { Spacer() - Button("Done") { dismiss() } + Button("Done") { + Task { await onComplete() } + dismiss() + } .buttonStyle(.bordered) .keyboardShortcut(.defaultAction) .padding(.bottom, SpacingTokens.lg) diff --git a/Echo/Sources/Features/Account/Views/E2ERecoveryView.swift b/Echo/Sources/Features/Account/Views/E2ERecoveryView.swift index 4029c672f..0156bb115 100644 --- a/Echo/Sources/Features/Account/Views/E2ERecoveryView.swift +++ b/Echo/Sources/Features/Account/Views/E2ERecoveryView.swift @@ -1,8 +1,10 @@ import SwiftUI +import UniformTypeIdentifiers /// Recovery flow: enter 24-word mnemonic + set a new master password. struct E2ERecoveryView: View { @Bindable var enrollmentManager: E2EEnrollmentManager + let onRecovered: () async -> Void @Environment(\.dismiss) private var dismiss @State private var mnemonicText = "" @@ -11,6 +13,7 @@ struct E2ERecoveryView: View { @State private var isProcessing = false @State private var errorMessage: String? @State private var isRecovered = false + @State private var showRecoveryFileImporter = false var body: some View { if isRecovered { @@ -43,6 +46,10 @@ struct E2ERecoveryView: View { Text("Separate words with spaces") .font(TypographyTokens.detail) .foregroundStyle(ColorTokens.Text.tertiary) + + Button("Import Recovery Key File…") { + showRecoveryFileImporter = true + } } Section("New Master Password") { @@ -74,6 +81,17 @@ struct E2ERecoveryView: View { .formStyle(.grouped) .scrollContentBackground(.hidden) .frame(width: 460, height: 420) + .fileImporter( + isPresented: $showRecoveryFileImporter, + allowedContentTypes: [.plainText] + ) { result in + switch result { + case .success(let url): + importRecoveryKey(from: url) + case .failure(let error): + errorMessage = error.localizedDescription + } + } } private var canRecover: Bool { @@ -92,12 +110,42 @@ struct E2ERecoveryView: View { do { try await enrollmentManager.recover(mnemonic: words, newPassword: newPassword) + SyncPreferences.setCredentialSyncEnabled(true) + await onRecovered() isRecovered = true } catch { errorMessage = error.localizedDescription } } + private func importRecoveryKey(from url: URL) { + let didAccess = url.startAccessingSecurityScopedResource() + defer { + if didAccess { + url.stopAccessingSecurityScopedResource() + } + } + + do { + let content = try String(contentsOf: url, encoding: .utf8) + let words = content + .lowercased() + .split(whereSeparator: { !$0.isLetter }) + .map(String.init) + .filter { $0.count > 1 } + + guard words.count >= 24 else { + errorMessage = "The selected file does not contain a valid 24-word recovery key." + return + } + + mnemonicText = Array(words.suffix(24)).joined(separator: " ") + errorMessage = nil + } catch { + errorMessage = error.localizedDescription + } + } + private var recoveredContent: some View { VStack(spacing: SpacingTokens.lg) { Spacer() diff --git a/Echo/Sources/Features/Account/Views/E2EUnlockView.swift b/Echo/Sources/Features/Account/Views/E2EUnlockView.swift index 89db884f1..c112e1fc0 100644 --- a/Echo/Sources/Features/Account/Views/E2EUnlockView.swift +++ b/Echo/Sources/Features/Account/Views/E2EUnlockView.swift @@ -4,6 +4,7 @@ import SwiftUI /// Shown when E2E is enrolled but the master key isn't in the local Keychain. struct E2EUnlockView: View { @Bindable var enrollmentManager: E2EEnrollmentManager + let onUnlock: () async -> Void @Environment(\.dismiss) private var dismiss @State private var password = "" @@ -13,52 +14,50 @@ struct E2EUnlockView: View { @State private var attemptCount = 0 var body: some View { - Form { - Section { - VStack(alignment: .leading, spacing: SpacingTokens.sm) { - Label("Unlock Credentials", systemImage: "lock.shield") - .font(TypographyTokens.headline) - - Text("Enter your master password to decrypt your synced credentials on this device.") - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Text.secondary) - } - - SecureField("", text: $password, prompt: Text("Master password")) - .onSubmit { Task { await unlock() } } - - if let errorMessage { - Text(errorMessage) - .font(TypographyTokens.detail) - .foregroundStyle(ColorTokens.Status.error) + SheetLayoutCustomFooter(title: "Unlock Credentials") { + Form { + Section { + PropertyRow( + title: "Master Password", + subtitle: "Enter your master password to decrypt synced credentials on this Mac." + ) { + SecureField("", text: $password, prompt: Text("Enter password")) + .textFieldStyle(.plain) + .multilineTextAlignment(.trailing) + .onSubmit { Task { await unlock() } } + } } } + .formStyle(.grouped) + .scrollContentBackground(.hidden) + } footer: { + if let errorMessage { + Text(errorMessage) + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Status.error) + .lineLimit(2) + } - Section { - HStack { - Button("Skip") { dismiss() } + Button("Skip") { dismiss() } - if attemptCount >= 2 { - Button("Forgot Password?") { showRecovery = true } - .font(TypographyTokens.formDescription) - } + if attemptCount >= 2 { + Button("Forgot Password?") { showRecovery = true } + } - Spacer() + Spacer() - Button("Unlock") { - Task { await unlock() } - } - .buttonStyle(.bordered) - .keyboardShortcut(.defaultAction) - .disabled(password.isEmpty || isProcessing) - } + Button("Unlock") { + Task { await unlock() } } + .buttonStyle(.bordered) + .keyboardShortcut(.defaultAction) + .disabled(password.isEmpty || isProcessing) } - .formStyle(.grouped) - .scrollContentBackground(.hidden) .frame(width: 400, height: 260) .sheet(isPresented: $showRecovery) { - E2ERecoveryView(enrollmentManager: enrollmentManager) + E2ERecoveryView(enrollmentManager: enrollmentManager) { + await onUnlock() + } } } @@ -69,6 +68,8 @@ struct E2EUnlockView: View { do { try await enrollmentManager.unlock(password: password) + SyncPreferences.setCredentialSyncEnabled(true) + await onUnlock() dismiss() } catch { attemptCount += 1 diff --git a/Echo/Sources/Features/Account/Views/SignedInAccountCard+Bindings.swift b/Echo/Sources/Features/Account/Views/SignedInAccountCard+Bindings.swift new file mode 100644 index 000000000..41a6a7186 --- /dev/null +++ b/Echo/Sources/Features/Account/Views/SignedInAccountCard+Bindings.swift @@ -0,0 +1,104 @@ +import SwiftUI + +extension AccountDetailSheet { + + // MARK: - Credential Sync Description + + var credentialSyncDescription: String { + if isCheckingEnrollment { + return "Checking status…" + } + if e2eManager.isEnrolled { + if e2eManager.isUnlocked { + return "Passwords encrypted end-to-end" + } else { + return "Enter your master password to sync passwords" + } + } + return "Encrypt passwords before syncing" + } + + // MARK: - Credential Sync Binding + + var credentialSyncBinding: Binding { + Binding( + get: { + e2eManager.isEnrolled && SyncPreferences.isCredentialSyncEnabled + }, + set: { newValue in + if newValue { + if e2eManager.isEnrolled { + if e2eManager.isUnlocked { + SyncPreferences.setCredentialSyncEnabled(true) + } else { + showE2EUnlock = true + } + } else { + showE2EEnrollment = true + } + } else { + SyncPreferences.setCredentialSyncEnabled(false) + } + } + ) + } + + // MARK: - Project Sync Binding + + func projectSyncBinding(for project: Project) -> Binding { + Binding( + get: { project.isSyncEnabled }, + set: { newValue in + if newValue { + // Enabling sync — check if server already has data + Task { await enableSyncOnProject(project) } + } else { + // Disabling sync — simple toggle + guard let store = AppDirector.shared.projectStore as ProjectStore?, + var updated = store.projects.first(where: { $0.id == project.id }) else { return } + updated.isSyncEnabled = false + Task { try? await store.updateProject(updated) } + } + } + ) + } + + /// Enable sync on a project, checking for server data first. + /// If both local and cloud data exist, shows the merge strategy prompt. + func enableSyncOnProject(_ project: Project) async { + guard let syncEngine = AppDirector.shared.syncEngine else { return } + + do { + let summary = try await syncEngine.checkSyncDataSummary(for: project) + + if summary.needsMergeDecision { + // Both sides have data — show merge strategy prompt + mergeStrategySummary = summary + mergeStrategyProject = project + showMergeStrategySheet = true + } else if summary.hasCloudData { + // Only cloud has data — pull from cloud + _ = await performSyncWithStrategy(project: project, strategy: .useCloud) + } else { + // Only local data or both empty — upload local + _ = await performSyncWithStrategy(project: project, strategy: .uploadLocal) + } + } catch { + // Fallback: just enable and upload (original behavior) + _ = await performSyncWithStrategy(project: project, strategy: .uploadLocal) + } + } + + // MARK: - Sync Collection Binding + + var hasSyncEnabledProjects: Bool { + AppDirector.shared.projectStore.projects.contains { $0.isSyncEnabled } + } + + func syncCollectionBinding(for collection: SyncCollection) -> Binding { + Binding( + get: { SyncPreferences.isEnabled(collection) }, + set: { SyncPreferences.setEnabled(collection, enabled: $0) } + ) + } +} diff --git a/Echo/Sources/Features/Account/Views/SignedInAccountCard+Components.swift b/Echo/Sources/Features/Account/Views/SignedInAccountCard+Components.swift new file mode 100644 index 000000000..2333e2aa0 --- /dev/null +++ b/Echo/Sources/Features/Account/Views/SignedInAccountCard+Components.swift @@ -0,0 +1,121 @@ +import SwiftUI + +extension SignedInAccountCard { + + // MARK: - Sync Summary (inline, one line) + + @ViewBuilder + var syncSummary: some View { + if let engine = syncEngine { + HStack(spacing: 4) { + switch engine.status { + case .idle: + if let lastSync = engine.lastSyncedAt { + Image(systemName: "checkmark.icloud") + .foregroundStyle(ColorTokens.Status.success) + Text("Synced \(lastSync, format: .relative(presentation: .named))") + } else { + Image(systemName: "icloud") + .foregroundStyle(ColorTokens.Text.tertiary) + Text("Sync available") + } + case .syncing: + ProgressView() + .controlSize(.mini) + Text("Syncing…") + case .error: + Image(systemName: "exclamationmark.icloud") + .foregroundStyle(ColorTokens.Status.error) + Text("Sync error — tap to retry") + case .offline: + Image(systemName: "icloud.slash") + .foregroundStyle(ColorTokens.Text.tertiary) + Text("Offline") + case .disabled: + Image(systemName: "icloud.slash") + .foregroundStyle(ColorTokens.Text.tertiary) + Text("Sync disabled") + } + } + .font(TypographyTokens.detail) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } + + @ViewBuilder + func syncRefreshButton(_ engine: SyncEngine) -> some View { + Button { + Task(name: "account-card-sync-refresh") { + await engine.syncNow() + } + } label: { + ZStack { + Circle() + .fill(Color.primary.opacity(isRefreshHovered ? 0.08 : 0)) + + Group { + if engine.status == .syncing { + ProgressView() + .controlSize(.mini) + } else { + Image(systemName: "arrow.clockwise") + .imageScale(.medium) + } + } + .foregroundStyle(ColorTokens.Text.primary) + } + .frame(width: 32, height: 32) + .contentShape(Circle()) + } + .buttonStyle(.plain) + .glassEffect(.regular.interactive(), in: .circle) + .help(engine.status == .syncing ? "Syncing" : "Sync Now") + .disabled(engine.status == .syncing || engine.status == .disabled) + .onHover { isRefreshHovered = $0 } + } + + // MARK: - Avatar + + @ViewBuilder + var accountAvatar: some View { + if let avatarURL = authState.currentUser?.avatarURL { + AsyncImage(url: avatarURL) { phase in + switch phase { + case .success(let image): + image + .resizable() + .scaledToFill() + .frame(width: 48, height: 48) + .clipShape(Circle()) + default: + initialsAvatar + } + } + } else { + initialsAvatar + } + } + + var initialsAvatar: some View { + ZStack { + Circle() + .fill(.quaternary) + .frame(width: 48, height: 48) + + Text(avatarInitials) + .font(TypographyTokens.statNumber) + .foregroundStyle(ColorTokens.Text.secondary) + } + } + + var avatarInitials: String { + let name = authState.currentUser?.displayName + ?? authState.currentUser?.email + ?? "U" + let parts = name.split(separator: " ") + if parts.count >= 2 { + return "\(parts[0].prefix(1))\(parts[1].prefix(1))".uppercased() + } + return String(name.prefix(2)).uppercased() + } +} diff --git a/Echo/Sources/Features/Account/Views/SignedInAccountCard+DetailSections.swift b/Echo/Sources/Features/Account/Views/SignedInAccountCard+DetailSections.swift new file mode 100644 index 000000000..adb0a776d --- /dev/null +++ b/Echo/Sources/Features/Account/Views/SignedInAccountCard+DetailSections.swift @@ -0,0 +1,135 @@ +import SwiftUI + +extension AccountDetailSheet { + + // MARK: - Profile + + var profileSection: some View { + Section("Profile") { + // Name + if isEditingName { + HStack { + TextField("", text: $editedName, prompt: Text("Your name")) + .textFieldStyle(.roundedBorder) + .onSubmit { saveDisplayName() } + + Button("Save") { saveDisplayName() } + .buttonStyle(.bordered) + .keyboardShortcut(.defaultAction) + .controlSize(.small) + .disabled(editedName.trimmingCharacters(in: .whitespaces).isEmpty) + + Button("Cancel") { isEditingName = false } + .controlSize(.small) + } + } else { + PropertyRow(title: "Name") { + HStack(spacing: SpacingTokens.xs) { + Text(authState.currentUser?.displayName ?? "Not set") + .foregroundStyle(authState.currentUser?.displayName != nil ? ColorTokens.Text.primary : ColorTokens.Text.tertiary) + Button { + editedName = authState.currentUser?.displayName ?? "" + isEditingName = true + } label: { + Image(systemName: "pencil") + .font(TypographyTokens.detail) + .foregroundStyle(ColorTokens.Text.tertiary) + } + .buttonStyle(.plain) + } + } + } + + // Email + if let email = authState.currentUser?.email { + PropertyRow(title: "Email") { + Text(email) + .foregroundStyle(ColorTokens.Text.secondary) + } + } + + // Auth Method + if let method = authState.currentUser?.authMethod { + PropertyRow(title: "Sign-in method") { + HStack(spacing: 4) { + authMethodIcon(method) + Text(method.displayName) + } + } + } + } + } + + // MARK: - Actions + + var actionsSection: some View { + Section { + HStack { + Button("Sign Out") { + Task { + await authState.signOut() + dismiss() + } + } + + Spacer() + + Button("Delete Account", role: .destructive) { + showDeleteConfirmation = true + } + .font(TypographyTokens.formDescription) + .buttonStyle(.bordered) + .tint(ColorTokens.Status.error) + } + .alert("Delete Account", isPresented: $showDeleteConfirmation) { + Button("Delete", role: .destructive) { + Task { + do { + try await authState.deleteAccount() + dismiss() + } catch { + return + } + } + } + Button("Cancel", role: .cancel) {} + } message: { + Text("This will permanently delete your account and all synced data. This action cannot be undone.") + } + + if let error = authState.error { + Text(error.localizedDescription) + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Status.error) + } + } + } + + // MARK: - Auth Method Icon + + @ViewBuilder + func authMethodIcon(_ method: AuthMethod) -> some View { + switch method { + case .google: + Image("GoogleLogo") + .resizable() + .scaledToFit() + .frame(width: 13, height: 13) + case .apple: + Image(systemName: "apple.logo") + .font(TypographyTokens.detail) + case .email: + Image(systemName: "envelope.fill") + .font(TypographyTokens.detail) + } + } + + // MARK: - Helpers + + func saveDisplayName() { + let name = editedName.trimmingCharacters(in: .whitespaces) + guard !name.isEmpty else { return } + isEditingName = false + Task { await authState.updateDisplayName(name) } + } +} diff --git a/Echo/Sources/Features/Account/Views/SignedInAccountCard+DetailSheet.swift b/Echo/Sources/Features/Account/Views/SignedInAccountCard+DetailSheet.swift new file mode 100644 index 000000000..cc45e820c --- /dev/null +++ b/Echo/Sources/Features/Account/Views/SignedInAccountCard+DetailSheet.swift @@ -0,0 +1,150 @@ +import SwiftUI + +// MARK: - Account Detail Sheet + +struct AccountDetailSheet: View { + @Bindable var authState: AuthState + var syncEngine: SyncEngine? + @Environment(\.dismiss) var dismiss + + @State var isEditingName = false + @State var editedName = "" + @State var showDeleteConfirmation = false + @State var showE2EEnrollment = false + @State var showE2EUnlock = false + @State var isCheckingEnrollment = false + + // Merge strategy prompt state + @State var mergeStrategySummary: SyncDataSummary? + @State var mergeStrategyProject: Project? + @State var showMergeStrategySheet = false + + // Credential conflict state + @State var showCredentialConflictSheet = false + + var e2eManager: E2EEnrollmentManager { + AppDirector.shared.e2eEnrollmentManager + } + + var body: some View { + Form { + profileSection + + if let syncEngine { + projectsSyncSection + syncSection(syncEngine) + } + + actionsSection + } + .formStyle(.grouped) + .scrollContentBackground(.hidden) + .frame(width: 440, height: 700) + .task { await prepareSheetState() } + .onChange(of: syncEngine?.pendingCredentialConflicts.count ?? 0) { + if let conflicts = syncEngine?.pendingCredentialConflicts, !conflicts.isEmpty { + showCredentialConflictSheet = true + } + } + .sheet(isPresented: $showE2EEnrollment) { + E2EEnrollmentView(enrollmentManager: e2eManager) { + await handleCredentialSetupCompletion() + } + } + .sheet(isPresented: $showE2EUnlock) { + E2EUnlockView(enrollmentManager: e2eManager) { + await handleCredentialSetupCompletion() + } + } + .sheet(isPresented: $showMergeStrategySheet) { + if let summary = mergeStrategySummary, let project = mergeStrategyProject { + SyncMergeStrategySheet(summary: summary, projectName: project.name) { strategy in + Task { + let succeeded = await performSyncWithStrategy(project: project, strategy: strategy) + if succeeded { + await presentNextStartupRequirementIfNeeded() + } + } + } + } + } + .sheet(isPresented: $showCredentialConflictSheet) { + if let conflicts = syncEngine?.pendingCredentialConflicts, !conflicts.isEmpty { + CredentialConflictSheet(conflicts: conflicts) { useCloud in + syncEngine?.resolveAllCredentialConflicts(useCloud: useCloud) + } + } + } + } + + func prepareSheetState() async { + await refreshEnrollmentStatus() + if let conflicts = syncEngine?.pendingCredentialConflicts, !conflicts.isEmpty { + showCredentialConflictSheet = true + } + if e2eManager.isEnrolled && SyncPreferences.isCredentialSyncEnabled && !e2eManager.isUnlocked { + showE2EUnlock = true + return + } + await presentNextStartupRequirementIfNeeded() + } + + func refreshEnrollmentStatus() async { + isCheckingEnrollment = true + await e2eManager.checkEnrollmentStatus() + await e2eManager.tryAutoUnlock() + isCheckingEnrollment = false + } + + func handleCredentialSetupCompletion() async { + await presentNextStartupRequirementIfNeeded() + if !showMergeStrategySheet { + await syncEngine?.syncNow() + } + } + + func presentNextStartupRequirementIfNeeded() async { + guard let syncEngine else { return } + + do { + guard let requirement = try await syncEngine.nextStartupRequirement() else { return } + switch requirement.action { + case .promptForMerge: + mergeStrategySummary = requirement.summary + mergeStrategyProject = requirement.project + showMergeStrategySheet = true + case .pullCloud: + let succeeded = await performSyncWithStrategy(project: requirement.project, strategy: .useCloud) + if succeeded { + await presentNextStartupRequirementIfNeeded() + } + case .uploadLocal: + let succeeded = await performSyncWithStrategy(project: requirement.project, strategy: .uploadLocal) + if succeeded { + await presentNextStartupRequirementIfNeeded() + } + case .none: + break + } + } catch { + return + } + } + + func performSyncWithStrategy(project: Project, strategy: SyncMergeStrategy) async -> Bool { + guard let syncEngine else { return false } + do { + // Enable sync on the project first + if let store = AppDirector.shared.projectStore as ProjectStore?, + var updated = store.projects.first(where: { $0.id == project.id }) { + updated.isSyncEnabled = true + try await store.updateProject(updated) + } + try await syncEngine.performInitialUpload(for: project, strategy: strategy) + return true + } catch { + // Error will be reflected in sync status + return false + } + } +} diff --git a/Echo/Sources/Features/Account/Views/SignedInAccountCard+SyncSections.swift b/Echo/Sources/Features/Account/Views/SignedInAccountCard+SyncSections.swift new file mode 100644 index 000000000..187bf8715 --- /dev/null +++ b/Echo/Sources/Features/Account/Views/SignedInAccountCard+SyncSections.swift @@ -0,0 +1,124 @@ +import SwiftUI + +extension AccountDetailSheet { + + // MARK: - Sync + + func syncSection(_ engine: SyncEngine) -> some View { + Section { + PropertyRow(title: "Status", subtitle: syncStatusDescription(engine)) { + syncStatusAccessory(engine) + } + + credentialSyncToggle + + ForEach(SyncCollection.userToggleable, id: \.self) { collection in + PropertyRow( + title: collection.displayName, + subtitle: syncCollectionDescription(collection) + ) { + Toggle("", isOn: syncCollectionBinding(for: collection)) + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.small) + } + } + } header: { + Text("Cloud Sync") + } + } + + // MARK: - Credential Sync Toggle + + var credentialSyncToggle: some View { + PropertyRow(title: "Credentials", subtitle: credentialSyncDescription) { + HStack(spacing: SpacingTokens.xs) { + if isCheckingEnrollment { + ProgressView() + .controlSize(.mini) + } else if e2eManager.isEnrolled && !e2eManager.isUnlocked && SyncPreferences.isCredentialSyncEnabled { + Image(systemName: "lock.fill") + .font(TypographyTokens.detail) + .foregroundStyle(ColorTokens.Text.tertiary) + } + + Toggle("", isOn: credentialSyncBinding) + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.small) + } + } + } + + // MARK: - Projects + + var projectsSyncSection: some View { + Section("Projects") { + let projects = AppDirector.shared.projectStore.projects + if projects.isEmpty { + Text("No projects") + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Text.tertiary) + } else { + ForEach(projects) { project in + Toggle(isOn: projectSyncBinding(for: project)) { + Label { + Text(project.name) + } icon: { + Image(systemName: project.iconName ?? "folder.fill") + .foregroundStyle(project.color) + } + } + .toggleStyle(.switch) + .controlSize(.small) + } + } + } + } + + private func syncCollectionDescription(_ collection: SyncCollection) -> String { + switch collection { + case .identities: + if SyncPreferences.isCredentialSyncEnabled { + return "Saved login names and identity metadata. Passwords are synced through Credentials." + } + return "Saved login names and identity metadata. Passwords stay only on this Mac until Credentials is enabled." + default: + return collection.displayDescription + } + } + + private func syncStatusDescription(_ engine: SyncEngine) -> String { + switch engine.status { + case .idle: + if let lastSync = engine.lastSyncedAt { + return "Last synced \(lastSync.formatted(.relative(presentation: .named)))" + } + if hasSyncEnabledProjects { + return "Ready to sync your enabled projects." + } + return "No projects are currently selected for cloud sync." + case .syncing: + return "Syncing your selected projects now." + case .error(let message): + return message + case .offline: + return "Cloud sync is unavailable while Echo is offline." + case .disabled: + return "Sign in to enable cloud sync." + } + } + + @ViewBuilder + private func syncStatusAccessory(_ engine: SyncEngine) -> some View { + if engine.status.isSyncing { + ProgressView() + .controlSize(.small) + } else { + Button("Sync Now") { + Task { await engine.syncNow() } + } + .disabled(!hasSyncEnabledProjects) + } + } +} diff --git a/Echo/Sources/Features/Account/Views/SignedInAccountCard.swift b/Echo/Sources/Features/Account/Views/SignedInAccountCard.swift index e7770a366..9510603b4 100644 --- a/Echo/Sources/Features/Account/Views/SignedInAccountCard.swift +++ b/Echo/Sources/Features/Account/Views/SignedInAccountCard.swift @@ -7,6 +7,8 @@ struct SignedInAccountCard: View { var syncEngine: SyncEngine? @State private var showAccountSheet = false + @State private var lastAutoPresentedUserID: String? + @State var isRefreshHovered = false var body: some View { Section { @@ -17,461 +19,69 @@ struct SignedInAccountCard: View { .sheet(isPresented: $showAccountSheet) { AccountDetailSheet(authState: authState, syncEngine: syncEngine) } + .task(id: authState.currentUser?.userID) { + await presentAccountSheetIfSetupNeedsAttention() + } } // MARK: - Clickable Row private var accountRow: some View { - Button { - showAccountSheet = true - } label: { - HStack(spacing: SpacingTokens.md) { - accountAvatar - - VStack(alignment: .leading, spacing: 2) { - Text(authState.currentUser?.displayName ?? "Echo User") - .font(TypographyTokens.prominent) - .foregroundStyle(ColorTokens.Text.primary) - - if let email = authState.currentUser?.email { - Text(email) - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Text.secondary) - } - - syncSummary - } - - Spacer() - - Image(systemName: "chevron.right") - .font(TypographyTokens.labelBold) - .foregroundStyle(ColorTokens.Text.quaternary) - } - .padding(.vertical, SpacingTokens.xs) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - } - - // MARK: - Sync Summary (inline, one line) - - @ViewBuilder - private var syncSummary: some View { - if let engine = syncEngine { - HStack(spacing: 4) { - switch engine.status { - case .idle: - if let lastSync = engine.lastSyncedAt { - Image(systemName: "checkmark.icloud") - .foregroundStyle(ColorTokens.Status.success) - Text("Synced \(lastSync, format: .relative(presentation: .named))") - } else { - Image(systemName: "icloud") - .foregroundStyle(ColorTokens.Text.tertiary) - Text("Sync available") - } - case .syncing: - ProgressView() - .controlSize(.mini) - Text("Syncing…") - case .error: - Image(systemName: "exclamationmark.icloud") - .foregroundStyle(ColorTokens.Status.error) - Text("Sync error — tap to retry") - case .offline: - Image(systemName: "icloud.slash") - .foregroundStyle(ColorTokens.Text.tertiary) - Text("Offline") - case .disabled: - Image(systemName: "icloud.slash") - .foregroundStyle(ColorTokens.Text.tertiary) - Text("Sync disabled") - } - } - .font(TypographyTokens.detail) - .foregroundStyle(ColorTokens.Text.tertiary) - } - } - - // MARK: - Avatar - - @ViewBuilder - private var accountAvatar: some View { - if let avatarURL = authState.currentUser?.avatarURL { - AsyncImage(url: avatarURL) { phase in - switch phase { - case .success(let image): - image - .resizable() - .scaledToFill() - .frame(width: 48, height: 48) - .clipShape(Circle()) - default: - initialsAvatar - } - } - } else { - initialsAvatar - } - } - - private var initialsAvatar: some View { - ZStack { - Circle() - .fill(.quaternary) - .frame(width: 48, height: 48) - - Text(avatarInitials) - .font(TypographyTokens.statNumber) - .foregroundStyle(ColorTokens.Text.secondary) - } - } - - private var avatarInitials: String { - let name = authState.currentUser?.displayName - ?? authState.currentUser?.email - ?? "U" - let parts = name.split(separator: " ") - if parts.count >= 2 { - return "\(parts[0].prefix(1))\(parts[1].prefix(1))".uppercased() - } - return String(name.prefix(2)).uppercased() - } -} - -// MARK: - Account Detail Sheet - -private struct AccountDetailSheet: View { - @Bindable var authState: AuthState - var syncEngine: SyncEngine? - @Environment(\.dismiss) private var dismiss - - @State private var isEditingName = false - @State private var editedName = "" - @State private var showDeleteConfirmation = false - @State private var showE2EEnrollment = false - @State private var showE2EUnlock = false - - var body: some View { - Form { - profileSection - - if let syncEngine { - projectsSyncSection - syncSection(syncEngine) - credentialSyncSection - } - - actionsSection - } - .formStyle(.grouped) - .scrollContentBackground(.hidden) - .frame(width: 440, height: 700) - .sheet(isPresented: $showE2EEnrollment) { - E2EEnrollmentView(enrollmentManager: AppDirector.shared.e2eEnrollmentManager) - } - .sheet(isPresented: $showE2EUnlock) { - E2EUnlockView(enrollmentManager: AppDirector.shared.e2eEnrollmentManager) - } - } - - // MARK: - Profile - - private var profileSection: some View { - Section("Profile") { - // Name - if isEditingName { - HStack { - TextField("", text: $editedName, prompt: Text("Your name")) - .textFieldStyle(.roundedBorder) - .onSubmit { saveDisplayName() } - - Button("Save") { saveDisplayName() } - .buttonStyle(.bordered) - .keyboardShortcut(.defaultAction) - .controlSize(.small) - .disabled(editedName.trimmingCharacters(in: .whitespaces).isEmpty) - - Button("Cancel") { isEditingName = false } - .controlSize(.small) - } - } else { - PropertyRow(title: "Name") { - HStack(spacing: SpacingTokens.xs) { - Text(authState.currentUser?.displayName ?? "Not set") - .foregroundStyle(authState.currentUser?.displayName != nil ? ColorTokens.Text.primary : ColorTokens.Text.tertiary) - Button { - editedName = authState.currentUser?.displayName ?? "" - isEditingName = true - } label: { - Image(systemName: "pencil") - .font(TypographyTokens.detail) - .foregroundStyle(ColorTokens.Text.tertiary) - } - .buttonStyle(.plain) - } - } - } - - // Email - if let email = authState.currentUser?.email { - PropertyRow(title: "Email") { - Text(email) - .foregroundStyle(ColorTokens.Text.secondary) - } - } - - // Auth Method - if let method = authState.currentUser?.authMethod { - PropertyRow(title: "Sign-in method") { - HStack(spacing: 4) { - authMethodIcon(method) - Text(method.displayName) - } - } - } - } - } + ZStack(alignment: .trailing) { + Button { + showAccountSheet = true + } label: { + HStack(spacing: SpacingTokens.md) { + accountAvatar - // MARK: - Sync - - private func syncSection(_ engine: SyncEngine) -> some View { - Section("Cloud Sync") { - // Status + Sync Now - HStack { - Label { - switch engine.status { - case .idle: - if let lastSync = engine.lastSyncedAt { - Text("Last synced \(lastSync, format: .relative(presentation: .named))") - } else if hasSyncEnabledProjects { - Text("Sync enabled") - } else { - Text("No projects synced") + VStack(alignment: .leading, spacing: 2) { + Text(authState.currentUser?.displayName ?? "Echo User") + .font(TypographyTokens.prominent) + .foregroundStyle(ColorTokens.Text.primary) + + if let email = authState.currentUser?.email { + Text(email) + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Text.secondary) } - case .syncing: - Text("Syncing…") - case .error(let message): - Text(message) - .lineLimit(2) - case .offline: - Text("Offline") - case .disabled: - Text("Sync disabled") - } - } icon: { - switch engine.status { - case .idle: - Image(systemName: "checkmark.icloud") - .foregroundStyle(ColorTokens.Status.success) - case .syncing: - ProgressView() - .controlSize(.small) - case .error: - Image(systemName: "exclamationmark.icloud") - .foregroundStyle(ColorTokens.Status.error) - case .offline, .disabled: - Image(systemName: "icloud.slash") - .foregroundStyle(ColorTokens.Text.tertiary) - } - } - .font(TypographyTokens.formDescription) - .foregroundStyle(engine.status.isError ? ColorTokens.Status.error : ColorTokens.Text.secondary) - - Spacer() - - Button("Sync Now") { - Task { await engine.syncNow() } - } - .font(TypographyTokens.formDescription) - .disabled(engine.status.isSyncing) - } - // Collection toggles - ForEach(SyncCollection.userToggleable, id: \.self) { collection in - Toggle(isOn: syncCollectionBinding(for: collection)) { - Label { - VStack(alignment: .leading, spacing: 2) { - Text(collection.displayName) - Text(collection.displayDescription) - .font(TypographyTokens.detail) - .foregroundStyle(ColorTokens.Text.tertiary) - } - } icon: { - Image(systemName: collection.systemImage) - .foregroundStyle(ColorTokens.Text.secondary) + syncSummary } - } - .toggleStyle(.switch) - .controlSize(.small) - } - } - } - // MARK: - Projects - - private var projectsSyncSection: some View { - Section("Projects") { - let projects = AppDirector.shared.projectStore.projects - if projects.isEmpty { - Text("No projects") - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Text.tertiary) - } else { - ForEach(projects) { project in - Toggle(isOn: projectSyncBinding(for: project)) { - Label { - Text(project.name) - } icon: { - Image(systemName: project.iconName ?? "folder.fill") - .foregroundStyle(project.color) - } - } - .toggleStyle(.switch) - .controlSize(.small) - } - } - } - } + Spacer() - private func projectSyncBinding(for project: Project) -> Binding { - Binding( - get: { project.isSyncEnabled }, - set: { newValue in - guard let store = AppDirector.shared.projectStore as ProjectStore?, - var updated = store.projects.first(where: { $0.id == project.id }) else { return } - updated.isSyncEnabled = newValue - Task { - try? await store.updateProject(updated) - if newValue, let syncEngine = AppDirector.shared.syncEngine { - try? await syncEngine.performInitialUpload(for: updated) - } + Image(systemName: "chevron.right") + .font(TypographyTokens.labelBold) + .foregroundStyle(ColorTokens.Text.quaternary) } + .contentShape(Rectangle()) } - ) - } + .buttonStyle(.plain) - private var hasSyncEnabledProjects: Bool { - AppDirector.shared.projectStore.projects.contains { $0.isSyncEnabled } - } - - private func syncCollectionBinding(for collection: SyncCollection) -> Binding { - Binding( - get: { SyncPreferences.isEnabled(collection) }, - set: { SyncPreferences.setEnabled(collection, enabled: $0) } - ) - } - - // MARK: - Actions - - private var actionsSection: some View { - Section { - HStack { - Button("Sign Out") { - Task { - await authState.signOut() - dismiss() - } - } - - Spacer() - - Button("Delete Account", role: .destructive) { - showDeleteConfirmation = true - } - .font(TypographyTokens.formDescription) - } - .alert("Delete Account", isPresented: $showDeleteConfirmation) { - Button("Delete", role: .destructive) { - Task { - try? await authState.deleteAccount() - dismiss() - } - } - Button("Cancel", role: .cancel) {} - } message: { - Text("This will permanently delete your account and all synced data. This action cannot be undone.") + if let syncEngine { + syncRefreshButton(syncEngine) + .padding(.trailing, 28) } } + .padding(.vertical, SpacingTokens.xs) } - // MARK: - Credential Sync (E2E) - - private var credentialSyncSection: some View { - let manager = AppDirector.shared.e2eEnrollmentManager - return Section("Credential Sync") { - if manager.isEnrolled { - if manager.isUnlocked { - Label { - Text("Passwords encrypted and synced") - } icon: { - Image(systemName: "lock.shield.fill") - .foregroundStyle(ColorTokens.Status.success) - } - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Text.secondary) - } else { - HStack { - Label { - Text("Locked — enter master password") - } icon: { - Image(systemName: "lock.fill") - .foregroundStyle(ColorTokens.Text.tertiary) - } - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Text.secondary) - - Spacer() - - Button("Unlock") { showE2EUnlock = true } - .font(TypographyTokens.formDescription) - } - } - } else { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("End-to-end encryption") - .font(TypographyTokens.formDescription) - Text("Encrypt database passwords before syncing") - .font(TypographyTokens.detail) - .foregroundStyle(ColorTokens.Text.tertiary) - } + private func presentAccountSheetIfSetupNeedsAttention() async { + guard let userID = authState.currentUser?.userID, + lastAutoPresentedUserID != userID else { return } - Spacer() + let e2eManager = AppDirector.shared.e2eEnrollmentManager + await e2eManager.checkEnrollmentStatus() + await e2eManager.tryAutoUnlock() - Button("Set Up") { showE2EEnrollment = true } - .font(TypographyTokens.formDescription) - } - } - } - } - - // MARK: - Auth Method Icon - - @ViewBuilder - private func authMethodIcon(_ method: AuthMethod) -> some View { - switch method { - case .google: - Image("GoogleLogo") - .resizable() - .scaledToFit() - .frame(width: 13, height: 13) - case .apple: - Image(systemName: "apple.logo") - .font(TypographyTokens.detail) - case .email: - Image(systemName: "envelope.fill") - .font(TypographyTokens.detail) - } - } + let needsCredentialUnlock = e2eManager.isEnrolled + && SyncPreferences.isCredentialSyncEnabled + && !e2eManager.isUnlocked + let needsMergeDecision = await syncEngine?.hasPendingMergeDecision() ?? false - // MARK: - Helpers + guard needsCredentialUnlock || needsMergeDecision else { return } - private func saveDisplayName() { - let name = editedName.trimmingCharacters(in: .whitespaces) - guard !name.isEmpty else { return } - isEditingName = false - Task { await authState.updateDisplayName(name) } + lastAutoPresentedUserID = userID + showAccountSheet = true } } diff --git a/Echo/Sources/Features/Account/Views/SyncMergeStrategySheet.swift b/Echo/Sources/Features/Account/Views/SyncMergeStrategySheet.swift new file mode 100644 index 000000000..bdbfc9161 --- /dev/null +++ b/Echo/Sources/Features/Account/Views/SyncMergeStrategySheet.swift @@ -0,0 +1,116 @@ +import SwiftUI + +/// Presented when a user enables sync on a project that has both local and cloud data. +/// Lets the user choose how to reconcile the two data sets. +struct SyncMergeStrategySheet: View { + let summary: SyncDataSummary + let projectName: String + let onChoose: (SyncMergeStrategy) -> Void + + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 0) { + Form { + Section { + VStack(alignment: .leading, spacing: SpacingTokens.sm) { + Label("Sync Conflict", systemImage: "arrow.triangle.2.circlepath") + .font(TypographyTokens.headline) + + Text("**\(projectName)** has data both on this device and in the cloud. How would you like to proceed?") + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Text.secondary) + } + .padding(.bottom, SpacingTokens.xs) + + // Data summary + HStack(spacing: SpacingTokens.lg) { + dataSummaryColumn( + title: "This Device", + icon: "laptopcomputer", + count: summary.localTotal + ) + + Divider() + .frame(height: 40) + + dataSummaryColumn( + title: "Cloud", + icon: "icloud", + count: summary.cloudDocuments + ) + } + .padding(.vertical, SpacingTokens.xs) + } + + Section { + strategyButton( + title: "Merge Both", + description: "Combine local and cloud data. If the same item exists in both, the most recent version wins.", + icon: "arrow.triangle.merge", + strategy: .merge + ) + + strategyButton( + title: "Use Cloud", + description: "Replace local data with what's in the cloud. Local-only items will be removed.", + icon: "icloud.and.arrow.down", + strategy: .useCloud + ) + + strategyButton( + title: "Upload Local", + description: "Push local data to the cloud, overwriting cloud versions where they conflict.", + icon: "icloud.and.arrow.up", + strategy: .uploadLocal + ) + } + + Section { + Button("Cancel") { dismiss() } + } + } + .formStyle(.grouped) + .scrollContentBackground(.hidden) + } + .frame(width: 420, height: 460) + } + + // MARK: - Components + + private func dataSummaryColumn(title: String, icon: String, count: Int) -> some View { + VStack(spacing: SpacingTokens.xs) { + Image(systemName: icon) + .font(TypographyTokens.prominent) + .foregroundStyle(ColorTokens.Text.secondary) + Text("\(count) items") + .font(TypographyTokens.labelBold) + Text(title) + .font(TypographyTokens.detail) + .foregroundStyle(ColorTokens.Text.tertiary) + } + .frame(maxWidth: .infinity) + } + + private func strategyButton(title: String, description: String, icon: String, strategy: SyncMergeStrategy) -> some View { + Button { + onChoose(strategy) + dismiss() + } label: { + Label { + VStack(alignment: .leading, spacing: 2) { + Text(title) + Text(description) + .font(TypographyTokens.detail) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } icon: { + Image(systemName: icon) + .foregroundStyle(ColorTokens.Text.secondary) + .frame(width: 20) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } +} diff --git a/Echo/Sources/Features/ActivityMonitor/Domain/ActivityMonitorViewModel.swift b/Echo/Sources/Features/ActivityMonitor/Domain/ActivityMonitorViewModel.swift index 5a7d86dfc..c2a6800fd 100644 --- a/Echo/Sources/Features/ActivityMonitor/Domain/ActivityMonitorViewModel.swift +++ b/Echo/Sources/Features/ActivityMonitor/Domain/ActivityMonitorViewModel.swift @@ -15,6 +15,7 @@ final class ActivityMonitorViewModel { @ObservationIgnored private let monitor: any DatabaseActivityMonitoring @ObservationIgnored private let mysqlSession: MySQLSession? @ObservationIgnored private var streamTask: Task? + @ObservationIgnored var activityEngine: ActivityEngine? let connectionSessionID: UUID let connectionID: UUID let databaseType: DatabaseType diff --git a/Echo/Sources/Features/ActivityMonitor/Views/ActivityMonitorSharedComponents.swift b/Echo/Sources/Features/ActivityMonitor/Views/ActivityMonitorSharedComponents.swift index 6c220b5ce..39169bf11 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/ActivityMonitorSharedComponents.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/ActivityMonitorSharedComponents.swift @@ -136,9 +136,9 @@ struct SectionContainer: View { .padding(.leading, SpacingTokens.xxxs) content() .background(ColorTokens.Background.secondary.opacity(0.3)) - .cornerRadius(8) + .clipShape(RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.medium)) .overlay( - RoundedRectangle(cornerRadius: 8) + RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.medium) .stroke(ColorTokens.Text.primary.opacity(0.05), lineWidth: 1) ) } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityIO.swift b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityIO.swift index a0c278bcb..bf7d28560 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityIO.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityIO.swift @@ -132,10 +132,13 @@ struct MySQLActivityIO: View { private func load() async { isLoading = true errorMessage = nil + let handle = viewModel.activityEngine?.begin("Loading file IO stats", connectionSessionID: viewModel.connectionSessionID) do { report = try await viewModel.loadMySQLFileIO() + handle?.succeed() } catch { errorMessage = error.localizedDescription + handle?.fail(error.localizedDescription) } isLoading = false } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityInnoDB.swift b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityInnoDB.swift index b24693d67..c44213abd 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityInnoDB.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityInnoDB.swift @@ -122,12 +122,15 @@ struct MySQLActivityInnoDB: View { private func load() async { isLoading = true errorMessage = nil + let handle = viewModel.activityEngine?.begin("Loading InnoDB status", connectionSessionID: viewModel.connectionSessionID) do { let status = try await viewModel.loadMySQLInnoDBStatus() statusSections = parseInnoDBStatus(status.statusText) expandedSections = Set(statusSections.prefix(3).map(\.title)) + handle?.succeed() } catch { errorMessage = error.localizedDescription + handle?.fail(error.localizedDescription) } isLoading = false } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityQueries.swift b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityQueries.swift index 331b94e1b..8bc42e251 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityQueries.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityQueries.swift @@ -154,6 +154,7 @@ struct MySQLActivityQueries: View { private func load() async { isLoading = true errorMessage = nil + let handle = viewModel.activityEngine?.begin("Loading query analysis", connectionSessionID: viewModel.connectionSessionID) do { let result: MySQLPerformanceReport switch selectedReport { @@ -165,8 +166,10 @@ struct MySQLActivityQueries: View { result = try await viewModel.loadMySQLFullTableScans() } report = result + handle?.succeed() } catch { errorMessage = error.localizedDescription + handle?.fail(error.localizedDescription) } isLoading = false } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityReplication.swift b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityReplication.swift index bcdc92d45..b4ff43a8c 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityReplication.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityReplication.swift @@ -178,13 +178,16 @@ struct MySQLActivityReplication: View { private func load() async { isLoading = true errorMessage = nil + let handle = viewModel.activityEngine?.begin("Loading replication status", connectionSessionID: viewModel.connectionSessionID) do { async let replica = viewModel.loadMySQLReplicaStatus() async let primary = viewModel.loadMySQLPrimaryStatus() replicaStatus = try await replica primaryStatus = try await primary + handle?.succeed() } catch { errorMessage = error.localizedDescription + handle?.fail(error.localizedDescription) } isLoading = false } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityWaits.swift b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityWaits.swift index 39d561388..22773e1b7 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityWaits.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/MySQL/MySQLActivityWaits.swift @@ -166,6 +166,7 @@ struct MySQLActivityWaits: View { private func load() async { isLoading = true errorMessage = nil + let handle = viewModel.activityEngine?.begin("Loading wait stats", connectionSessionID: viewModel.connectionSessionID) do { switch selectedTab { case .global: @@ -178,8 +179,10 @@ struct MySQLActivityWaits: View { name: "innodb_lock_waits" ) } + handle?.succeed() } catch { errorMessage = error.localizedDescription + handle?.fail(error.localizedDescription) } isLoading = false } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityBGWriter.swift b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityBGWriter.swift index 3bbff7766..f89589b54 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityBGWriter.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityBGWriter.swift @@ -3,6 +3,7 @@ import PostgresKit struct PostgresActivityBGWriter: View { let connectionID: UUID + var activityEngine: ActivityEngine? @Environment(EnvironmentState.self) private var environmentState @State private var stats: PostgresBGWriterStats? @@ -110,11 +111,14 @@ struct PostgresActivityBGWriter: View { guard let session = environmentState.sessionGroup.sessionForConnection(connectionID), let pg = session.session as? PostgresSession else { return } isLoading = true + let handle = activityEngine?.begin("Loading BGWriter stats", connectionSessionID: connectionID) defer { isLoading = false } do { stats = try await pg.client.metadata.fetchBGWriterStats() + handle?.succeed() } catch { stats = nil + handle?.fail(error.localizedDescription) } } } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityConfiguration.swift b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityConfiguration.swift index 675ce3769..1a82bad19 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityConfiguration.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityConfiguration.swift @@ -3,6 +3,7 @@ import PostgresKit struct PostgresActivityConfiguration: View { let connectionID: UUID + var activityEngine: ActivityEngine? @Environment(EnvironmentState.self) private var environmentState @State private var settings: [PostgresServerSetting] = [] @@ -130,11 +131,14 @@ struct PostgresActivityConfiguration: View { guard let session = environmentState.sessionGroup.sessionForConnection(connectionID), let pg = session.session as? PostgresSession else { return } isLoading = true + let handle = activityEngine?.begin("Loading server settings", connectionSessionID: connectionID) defer { isLoading = false } do { settings = try await pg.client.metadata.listServerSettings() + handle?.succeed() } catch { settings = [] + handle?.fail(error.localizedDescription) } } } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityIOStats.swift b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityIOStats.swift index 27225f705..6fee90b88 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityIOStats.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityIOStats.swift @@ -3,6 +3,7 @@ import PostgresKit struct PostgresActivityIOStats: View { let connectionID: UUID + var activityEngine: ActivityEngine? @Environment(EnvironmentState.self) private var environmentState @State private var stats: [PostgresTableIOStats] = [] @@ -111,11 +112,14 @@ struct PostgresActivityIOStats: View { guard let session = environmentState.sessionGroup.sessionForConnection(connectionID), let pg = session.session as? PostgresSession else { return } isLoading = true + let handle = activityEngine?.begin("Loading IO stats", connectionSessionID: connectionID) defer { isLoading = false } do { stats = try await pg.client.metadata.listTableIOStats(schema: schemaFilter) + handle?.succeed() } catch { stats = [] + handle?.fail(error.localizedDescription) } } } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityMonitorView.swift b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityMonitorView.swift index ad332e0f9..1aed14499 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityMonitorView.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityMonitorView.swift @@ -170,15 +170,15 @@ struct PostgresActivityMonitorView: View { case .replication: PostgresActivityReplication(info: snap.replicationInfo, sortOrder: $replicationSortOrder) case .ioStats: - PostgresActivityIOStats(connectionID: viewModel.connectionID) + PostgresActivityIOStats(connectionID: viewModel.connectionID, activityEngine: viewModel.activityEngine) case .wal: - PostgresActivityWAL(connectionID: viewModel.connectionID) + PostgresActivityWAL(connectionID: viewModel.connectionID, activityEngine: viewModel.activityEngine) case .bgWriter: - PostgresActivityBGWriter(connectionID: viewModel.connectionID) + PostgresActivityBGWriter(connectionID: viewModel.connectionID, activityEngine: viewModel.activityEngine) case .preparedTxns: - PostgresActivityPreparedTxns(connectionID: viewModel.connectionID) + PostgresActivityPreparedTxns(connectionID: viewModel.connectionID, activityEngine: viewModel.activityEngine) case .configuration: - PostgresActivityConfiguration(connectionID: viewModel.connectionID) + PostgresActivityConfiguration(connectionID: viewModel.connectionID, activityEngine: viewModel.activityEngine) } } else { EmptyTablePlaceholder() diff --git a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityPreparedTxns.swift b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityPreparedTxns.swift index 990eed560..0ff4eaae3 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityPreparedTxns.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityPreparedTxns.swift @@ -3,6 +3,7 @@ import PostgresKit struct PostgresActivityPreparedTxns: View { let connectionID: UUID + var activityEngine: ActivityEngine? @Environment(EnvironmentState.self) private var environmentState @State private var transactions: [PostgresPreparedTransaction] = [] @@ -118,11 +119,14 @@ struct PostgresActivityPreparedTxns: View { guard let session = environmentState.sessionGroup.sessionForConnection(connectionID), let pg = session.session as? PostgresSession else { return } isLoading = true + let handle = activityEngine?.begin("Loading prepared transactions", connectionSessionID: connectionID) defer { isLoading = false } do { transactions = try await pg.client.metadata.listPreparedTransactions() + handle?.succeed() } catch { transactions = [] + handle?.fail(error.localizedDescription) } } diff --git a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityWAL.swift b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityWAL.swift index 5a9e49414..5b79a8df7 100644 --- a/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityWAL.swift +++ b/Echo/Sources/Features/ActivityMonitor/Views/Postgres/PostgresActivityWAL.swift @@ -3,6 +3,7 @@ import PostgresKit struct PostgresActivityWAL: View { let connectionID: UUID + var activityEngine: ActivityEngine? @Environment(EnvironmentState.self) private var environmentState @State private var stats: PostgresWALStats? @@ -118,11 +119,14 @@ struct PostgresActivityWAL: View { guard let session = environmentState.sessionGroup.sessionForConnection(connectionID), let pg = session.session as? PostgresSession else { return } isLoading = true + let handle = activityEngine?.begin("Loading WAL stats", connectionSessionID: connectionID) defer { isLoading = false } do { stats = try await pg.client.metadata.fetchWALStats() + handle?.succeed() } catch { stats = nil + handle?.fail(error.localizedDescription) } } } diff --git a/Echo/Sources/Features/AppHost/Domain/AppDirector.swift b/Echo/Sources/Features/AppHost/Domain/AppDirector.swift index fdb947371..14eac934e 100644 --- a/Echo/Sources/Features/AppHost/Domain/AppDirector.swift +++ b/Echo/Sources/Features/AppHost/Domain/AppDirector.swift @@ -38,6 +38,7 @@ final class AppDirector { @ObservationIgnored let resultSpoolManager: ResultSpooler @ObservationIgnored let diagramCacheStore: DiagramCacheStore @ObservationIgnored let diagramKeyStore: DiagramEncryptionKeyStore + @ObservationIgnored let objectBrowserCacheStore: ObjectBrowserCacheStore @ObservationIgnored let activityEngine: ActivityEngine @ObservationIgnored let authState: AuthState @ObservationIgnored let syncEngine: SyncEngine? @@ -64,8 +65,12 @@ final class AppDirector { let diagramConfig = DiagramCacheStore.Configuration(rootDirectory: cacheRoot) let keyStore = DiagramEncryptionKeyStore() let cacheManager = DiagramCacheStore(configuration: diagramConfig) + let objectBrowserCacheStore = ObjectBrowserCacheStore( + configuration: .init(rootDirectory: ObjectBrowserCacheStore.defaultRootDirectory()) + ) self.diagramCacheStore = cacheManager self.diagramKeyStore = keyStore + self.objectBrowserCacheStore = objectBrowserCacheStore // Initialize modular stores let projectRepository = ProjectRepository(diskStore: ProjectDiskStore()) @@ -78,7 +83,11 @@ final class AppDirector { ) self.connectionStore = ConnectionStore(repository: connectionRepository) self.identityRepository = IdentityRepository(connectionStore: connectionStore) - self.schemaDiscoveryEngine = MetadataDiscoveryEngine(identityRepository: identityRepository, connectionStore: connectionStore) + self.schemaDiscoveryEngine = MetadataDiscoveryEngine( + identityRepository: identityRepository, + connectionStore: connectionStore, + objectBrowserCacheStore: objectBrowserCacheStore + ) self.bookmarkRepository = BookmarkRepository() self.historyRepository = HistoryRepository() @@ -137,7 +146,8 @@ final class AppDirector { historyRepository: historyRepository, resultSpoolManager: resultSpoolManager, diagramCacheStore: cacheManager, - diagramKeyStore: keyStore + diagramKeyStore: keyStore, + objectBrowserCacheStore: objectBrowserCacheStore ) let projectStoreRef = self.projectStore @@ -155,6 +165,9 @@ final class AppDirector { schemaDiscoveryEngine.onEnqueuePrefetch = { @MainActor [weak self] session in await self?.environmentState.enqueuePrefetchForSessionIfNeeded(session) } + schemaDiscoveryEngine.cacheLimitProvider = { @MainActor [weak self] in + self?.projectStore.globalSettings.objectBrowserCacheMaxBytes ?? 512 * 1_024 * 1_024 + } // Setup cross-domain providers for DiagramBuilder after EnvironmentState is initialized diagramBuilder.globalSettingsProvider = { @MainActor [weak self] in @@ -192,6 +205,8 @@ final class AppDirector { print("Failed to load modular stores: \(error)") } + await environmentState.migrateLegacyObjectBrowserCachesIfNeeded() + await environmentState.load() await authState.restoreSession() @@ -353,6 +368,7 @@ final class AppDirector { private func startSync() async { guard let syncEngine else { return } + guard await authState.ensureSupabaseSession() else { return } // If the user changed since last sign-in, reset sync state let currentUserID = authState.currentUser?.userID diff --git a/Echo/Sources/Features/AppHost/Domain/State/AppState.swift b/Echo/Sources/Features/AppHost/Domain/State/AppState.swift index 98897e100..56c10e349 100644 --- a/Echo/Sources/Features/AppHost/Domain/State/AppState.swift +++ b/Echo/Sources/Features/AppHost/Domain/State/AppState.swift @@ -4,11 +4,16 @@ import SwiftUI /// Centralized application state management @Observable final class AppState: @unchecked Sendable { + struct StructureScriptPreviewData { + let statements: [String] + } + // MARK: - UI State var isLoading = false var currentError: DatabaseError? var showingError = false var activeSheet: ActiveSheet? + var structureScriptData: StructureScriptPreviewData? var showTabOverview = false var showInfoSidebar = false var workspaceSidebarVisibility: NavigationSplitViewVisibility = .automatic @@ -90,7 +95,13 @@ import SwiftUI activeSheet = sheet } + func showStructureScriptPreview(statements: [String]) { + structureScriptData = StructureScriptPreviewData(statements: statements) + activeSheet = .structureScriptPreview + } + func dismissSheet() { + structureScriptData = nil activeSheet = nil } @@ -136,6 +147,7 @@ enum ActiveSheet: String, Identifiable { case preferences case about case exportData + case structureScriptPreview var id: String { rawValue diff --git a/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState+Connections.swift b/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState+Connections.swift index 8d21694d7..1ac1486ca 100644 --- a/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState+Connections.swift +++ b/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState+Connections.swift @@ -6,6 +6,16 @@ extension EnvironmentState { // MARK: - Session Management func connect(to connection: SavedConnection) { + if let existingSession = sessionGroup.sessionForConnection(connection.id) { + sessionGroup.setActiveSession(existingSession.id) + connectionStore.selectedConnectionID = connection.id + navigationStore.navigationState.selectConnection(connection) + if let databaseName = existingSession.sidebarFocusedDatabase { + navigationStore.navigationState.selectDatabase(databaseName) + } + navigationStore.revealExplorerConnection(connection.id) + return + } connectToNewSession(to: connection) } @@ -145,6 +155,34 @@ extension EnvironmentState { await schemaDiscoveryEngine.preloadStructure(for: connection, overridePassword: overridePassword) } + func objectBrowserCacheUsageBytes() async -> UInt64 { + await objectBrowserCacheStore.currentUsageBytes() + } + + func clearObjectBrowserCache() async { + await objectBrowserCacheStore.removeAll() + + for index in connectionStore.connections.indices { + connectionStore.connections[index].cachedStructure = nil + connectionStore.connections[index].cachedStructureUpdatedAt = nil + } + try? await connectionStore.saveConnections() + + for session in sessionGroup.activeSessions { + session.clearMetadataCacheState() + } + } + + func migrateLegacyObjectBrowserCachesIfNeeded() async { + let limitBytes = projectStore.globalSettings.objectBrowserCacheMaxBytes + for connection in connectionStore.connections { + await objectBrowserCacheStore.migrateLegacyCacheIfNeeded( + from: connection, + limitBytes: limitBytes + ) + } + } + // MARK: - Bookmarks func bookmarks(for connectionID: UUID) -> [Bookmark] { diff --git a/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState+TabManagement.swift b/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState+TabManagement.swift index f170d49fe..e37f73671 100644 --- a/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState+TabManagement.swift +++ b/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState+TabManagement.swift @@ -369,6 +369,24 @@ extension EnvironmentState { } } + func openInPsql(for session: ConnectionSession? = nil, database: String? = nil) { + let targetSession = session ?? sessionGroup.activeSession ?? sessionGroup.activeSessions.first + guard let targetSession else { return } + let requestedDatabase = (database ?? targetSession.sidebarFocusedDatabase ?? targetSession.connection.database) + .trimmingCharacters(in: .whitespacesAndNewlines) + let effectiveDatabase = requestedDatabase.isEmpty ? "postgres" : requestedDatabase + let connection = targetSession.connection + + Task { + await PostgresTerminalLauncher.openInTerminal( + host: connection.host, + port: connection.port, + username: connection.username, + database: effectiveDatabase + ) + } + } + func openJobQueueTab(for session: ConnectionSession, selectJobID: String? = nil) { // Reuse existing Jobs tab for this session if one exists if let existingTab = tabStore.tabs.first(where: { $0.kind == .jobQueue && $0.connectionSessionID == session.id }) { diff --git a/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState.swift b/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState.swift index a39b234d5..3cb50e9c3 100644 --- a/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState.swift +++ b/Echo/Sources/Features/AppHost/Domain/State/EnvironmentState.swift @@ -93,6 +93,7 @@ final class EnvironmentState { @ObservationIgnored let resultSpoolManager: ResultSpooler @ObservationIgnored let diagramCacheStore: DiagramCacheStore @ObservationIgnored let diagramKeyStore: DiagramEncryptionKeyStore + @ObservationIgnored let objectBrowserCacheStore: ObjectBrowserCacheStore @ObservationIgnored let dedicatedConnectionGate = AsyncSemaphore(limit: 3) @ObservationIgnored private var diagramRefreshTask: Task? @@ -114,7 +115,8 @@ final class EnvironmentState { historyRepository: HistoryRepository, resultSpoolManager: ResultSpooler, diagramCacheStore: DiagramCacheStore, - diagramKeyStore: DiagramEncryptionKeyStore + diagramKeyStore: DiagramEncryptionKeyStore, + objectBrowserCacheStore: ObjectBrowserCacheStore ) { self.projectStore = projectStore self.connectionStore = connectionStore @@ -130,6 +132,7 @@ final class EnvironmentState { self.resultSpoolManager = resultSpoolManager self.diagramCacheStore = diagramCacheStore self.diagramKeyStore = diagramKeyStore + self.objectBrowserCacheStore = objectBrowserCacheStore self.tabStore.delegate = self setupBindings() @@ -254,6 +257,17 @@ final class EnvironmentState { session: session, spoolManager: resultSpoolManager ) + if let cachedEntry = await objectBrowserCacheStore.entry(for: connection) { + connectionSession.databaseStructure = cachedEntry.structure + connectionSession.hydrateMetadataFreshnessFromCacheStructure() + connectionSession.structureLoadingState = .loading(progress: 0) + connectionSession.structureLoadingMessage = "Refreshing cached metadata…" + } else if let legacyStructure = connection.cachedStructure { + connectionSession.databaseStructure = legacyStructure + connectionSession.hydrateMetadataFreshnessFromCacheStructure() + connectionSession.structureLoadingState = .loading(progress: 0) + connectionSession.structureLoadingMessage = "Refreshing cached metadata…" + } // Transition: pending → active session pendingConnections.removeAll { $0.id == connection.id } @@ -264,13 +278,10 @@ final class EnvironmentState { detachedJobQueueViewModels.removeValue(forKey: oldSession.id) } - // Start structure load BEFORE adding session to the sidebar. - // This way the load runs in parallel with the sidebar rendering the new session. + sessionGroup.addSession(connectionSession) startStructureLoadTask(for: connectionSession) Task { await connectionSession.refreshPermissions() } connectionSession.startHealthCheck() - - sessionGroup.addSession(connectionSession) connectionStates[connection.id] = .connected recordRecentConnection(for: connection, databaseName: connectionSession.sidebarFocusedDatabase) notificationEngine?.post(category: .connectionConnected, message: "Connected to \(displayName)") diff --git a/Echo/Sources/Features/AppHost/Domain/State/NavigationStore.swift b/Echo/Sources/Features/AppHost/Domain/State/NavigationStore.swift index 8ec9d8b18..f6bf81d5e 100644 --- a/Echo/Sources/Features/AppHost/Domain/State/NavigationStore.swift +++ b/Echo/Sources/Features/AppHost/Domain/State/NavigationStore.swift @@ -8,6 +8,8 @@ final class NavigationStore { // MARK: - State var navigationState = NavigationState() var pendingExplorerFocus: ExplorerFocus? + var pendingExplorerRevealConnectionID: UUID? + var pendingExplorerRevealRequestID = 0 var isWorkspaceWindowKey = false var isManageConnectionsPresented = false var showNewProjectSheet = false @@ -30,6 +32,11 @@ final class NavigationStore { func clearExplorerFocus() { self.pendingExplorerFocus = nil } + + func revealExplorerConnection(_ connectionID: UUID) { + pendingExplorerRevealConnectionID = connectionID + pendingExplorerRevealRequestID &+= 1 + } func updateInspectorWidth(_ width: CGFloat, min minWidth: CGFloat, max maxWidth: CGFloat) { let clamped = max(minWidth, min(maxWidth, width)) diff --git a/Echo/Sources/Features/AppHost/EchoApp+ConnectMenu.swift b/Echo/Sources/Features/AppHost/EchoApp+ConnectMenu.swift index ca0eff3dc..7466bfbb5 100644 --- a/Echo/Sources/Features/AppHost/EchoApp+ConnectMenu.swift +++ b/Echo/Sources/Features/AppHost/EchoApp+ConnectMenu.swift @@ -150,7 +150,7 @@ struct ConnectMenuCommands: Commands { } private func availableDatabases(for session: ConnectionSession) -> [DatabaseInfo] { - let source = session.databaseStructure?.databases ?? session.connection.cachedStructure?.databases ?? [] + let source = session.databaseStructure?.databases ?? [] var deduplicated: [DatabaseInfo] = [] var seen: Set = [] @@ -206,20 +206,7 @@ struct ConnectMenuCommands: Commands { @ViewBuilder private func connectionIcon(for connection: SavedConnection) -> some View { - if let logoData = connection.logo, - let nsImage = NSImage(data: logoData) { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) - .clipShape(RoundedRectangle(cornerRadius: 3, style: .continuous)) - } else { - Image(connection.databaseType.iconName) - .resizable() - .renderingMode(.template) - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) - } + DatabaseTypeIcon(databaseType: connection.databaseType, presentation: .menu) } private func connectionMenuItems(parentID: UUID?, projectID: UUID?) -> AnyView { @@ -241,20 +228,7 @@ struct ConnectMenuCommands: Commands { Label { Text(displayName(for: connection)) } icon: { - if let logoData = connection.logo, - let nsImage = NSImage(data: logoData) { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) - .clipShape(RoundedRectangle(cornerRadius: 3, style: .continuous)) - } else { - Image(connection.databaseType.iconName) - .resizable() - .renderingMode(.template) - .aspectRatio(contentMode: .fit) - .frame(width: 14, height: 14) - } + DatabaseTypeIcon(databaseType: connection.databaseType, presentation: .menu) } } } diff --git a/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ConnectionDashboard/ConnectionDashboardView+Actions.swift b/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ConnectionDashboard/ConnectionDashboardView+Actions.swift index cdbe02843..bb0058516 100644 --- a/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ConnectionDashboard/ConnectionDashboardView+Actions.swift +++ b/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ConnectionDashboard/ConnectionDashboardView+Actions.swift @@ -46,6 +46,12 @@ struct ConnectionDashboardTools: View { } directAction: { environmentState.openPSQLTab(for: session) } + + DashboardToolCard(icon: "apple.terminal", label: "psql", menuItems: databases.map(\.name)) { db in + environmentState.openInPsql(for: session, database: db) + } directAction: { + environmentState.openInPsql(for: session) + } } // MARK: - MySQL diff --git a/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ConnectionDashboard/ConnectionDashboardView.swift b/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ConnectionDashboard/ConnectionDashboardView.swift index f5141c69a..3e675b464 100644 --- a/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ConnectionDashboard/ConnectionDashboardView.swift +++ b/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ConnectionDashboard/ConnectionDashboardView.swift @@ -65,11 +65,11 @@ struct ConnectionDashboardHeader: View { RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(session.connection.color.opacity(0.1)) .frame(width: 48, height: 48) - Image(session.connection.databaseType.iconName) - .resizable() - .aspectRatio(contentMode: .fit) + DatabaseTypeIcon( + databaseType: session.connection.databaseType, + tint: session.connection.color + ) .frame(width: 24, height: 24) - .foregroundStyle(session.connection.color) } } } diff --git a/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ServerConnectionPlaceholderView.swift b/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ServerConnectionPlaceholderView.swift index 804e768b7..764cfe67d 100644 --- a/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ServerConnectionPlaceholderView.swift +++ b/Echo/Sources/Features/AppHost/Views/Tabs/EditorContainer/ServerConnectionPlaceholderView.swift @@ -136,15 +136,10 @@ private struct RecentConnectionRow: View { } private var icon: some View { - ZStack { - RoundedRectangle(cornerRadius: SpacingTokens.xs, style: .continuous) - .fill(iconColor.opacity(0.12)) - .frame(width: 32, height: 32) - Image(connection.databaseType.iconName) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: SpacingTokens.md, height: SpacingTokens.md) - .foregroundStyle(iconColor) - } + DatabaseTypeIcon( + databaseType: connection.databaseType, + tint: iconColor, + presentation: .landingRecent + ) } } diff --git a/Echo/Sources/Features/AppHost/Views/Tabs/TabOverview/TabOverviewServerGroup.swift b/Echo/Sources/Features/AppHost/Views/Tabs/TabOverview/TabOverviewServerGroup.swift index 111c305b9..ebfb6e227 100644 --- a/Echo/Sources/Features/AppHost/Views/Tabs/TabOverview/TabOverviewServerGroup.swift +++ b/Echo/Sources/Features/AppHost/Views/Tabs/TabOverview/TabOverviewServerGroup.swift @@ -22,9 +22,11 @@ extension TabOverviewView { Text(group.connection.connectionName) .font(TypographyTokens.displayLarge.weight(.bold)) } icon: { - Image(systemName: group.connection.databaseType.iconName) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(isActiveServer ? ColorTokens.accent : ColorTokens.Text.secondary) + DatabaseTypeIcon( + databaseType: group.connection.databaseType, + tint: isActiveServer ? ColorTokens.accent : ColorTokens.Text.secondary + ) + .frame(width: SpacingTokens.md, height: SpacingTokens.md) } if group.connection.databaseType.isBeta { diff --git a/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceContentView.swift b/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceContentView.swift index 5e0c3fdcb..324521604 100644 --- a/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceContentView.swift +++ b/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceContentView.swift @@ -31,6 +31,38 @@ struct WorkspaceContentView: View { } } } + .sheet( + item: Binding( + get: { appState.activeSheet }, + set: { newValue in + if let newValue { + appState.activeSheet = newValue + } else { + appState.dismissSheet() + } + } + ) + ) { sheet in + switch sheet { + case .structureScriptPreview: + if let data = appState.structureScriptData { + StructureScriptPreviewSheet( + context: SQLPopoutContext( + sql: data.statements.joined(separator: "\n\n"), + title: "Script Preview" + ) + ) { sql, database in + if let session = environmentState.sessionGroup.sessionForConnection(tab.connection.id) { + environmentState.openQueryTab(for: session, presetQuery: sql, database: database) + } else { + environmentState.openQueryTab(presetQuery: sql, database: database) + } + } + } + default: + EmptyView() + } + } } // MARK: - Content Resolution (switch-based to help type-checker) @@ -40,7 +72,11 @@ struct WorkspaceContentView: View { switch tab.kind { case .structure: if let vm = tab.structureEditor { - TableStructureEditorView(tab: tab, viewModel: vm).background(ColorTokens.Background.primary) + ZStack { + TableStructureEditorView(tab: tab, viewModel: vm) + .background(ColorTokens.Background.primary) + TableStructureSheetHost(tab: tab, viewModel: vm) + } } case .diagram: if let vm = tab.diagram { diff --git a/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceTabContainerView+BatchExecution.swift b/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceTabContainerView+BatchExecution.swift index a73a6a105..6771b7e29 100644 --- a/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceTabContainerView+BatchExecution.swift +++ b/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceTabContainerView+BatchExecution.swift @@ -123,6 +123,7 @@ extension WorkspaceTabContainerView { await MainActor.run { queryState.errorMessage = nil + queryState.prefersMessagesAfterExecution = batchResultsPreferMessages(batches, databaseType: tab.connection.databaseType) queryState.startExecution() queryState.setExecutingTask(task) environmentState.dataInspectorContent = nil @@ -214,4 +215,10 @@ extension WorkspaceTabContainerView { state.batchResultMetadata = batchLabels } } + + private func batchResultsPreferMessages(_ batches: [String], databaseType: DatabaseType) -> Bool { + batches.allSatisfy { + QueryStatementClassifier.isLikelyMessageOnlyStatement($0, databaseType: databaseType) + } + } } diff --git a/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceTabContainerView+Execution.swift b/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceTabContainerView+Execution.swift index e5e927f01..a8b32f787 100644 --- a/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceTabContainerView+Execution.swift +++ b/Echo/Sources/Features/AppHost/Views/Tabs/WorkspaceContainer/WorkspaceTabContainerView+Execution.swift @@ -132,6 +132,12 @@ extension WorkspaceTabContainerView { } let inferredObject = inferPrimaryObjectName(from: effectiveSQL) + let prefersMessagesAfterExecution = + (tab.connection.databaseType == .microsoftSQL && queryState.statisticsEnabled) || + QueryStatementClassifier.isLikelyMessageOnlyStatement( + trimmedSQL, + databaseType: tab.connection.databaseType + ) await MainActor.run { queryState.updateClipboardObjectName(inferredObject) } @@ -204,31 +210,11 @@ extension WorkspaceTabContainerView { } await MainActor.run { - // Surface server info messages (statistics, warnings, etc.) - for serverMsg in result.serverMessages { - let severity: QueryExecutionMessage.Severity = serverMsg.kind == .error ? .error : .info - state.appendMessage( - message: serverMsg.message, - severity: severity, - metadata: serverMsg.number != 0 ? ["msgNumber": "\(serverMsg.number)"] : [:] - ) - } - - var metadata: [String: String] = [ - "rows": "\(result.rows.count)" - ] - let columnNames = result.columns.map(\.name).joined(separator: ", ") - if !columnNames.isEmpty { - metadata["columns"] = columnNames - } - if let commandTag = result.commandTag, !commandTag.isEmpty { - metadata["commandTag"] = commandTag - } - - state.appendMessage( - message: "Returned \(result.rows.count) row\(result.rows.count == 1 ? "" : "s")", - severity: .info, - metadata: metadata + appendResultMessages( + for: result, + originalSQL: trimmedSQL, + databaseType: tab.connection.databaseType, + state: state ) appState.addToQueryHistory( effectiveSQL, @@ -285,6 +271,7 @@ extension WorkspaceTabContainerView { await MainActor.run { queryState.errorMessage = nil + queryState.prefersMessagesAfterExecution = prefersMessagesAfterExecution queryState.startExecution() queryState.setExecutingTask(task) environmentState.dataInspectorContent = nil @@ -312,4 +299,78 @@ extension WorkspaceTabContainerView { return [:] } } + + @MainActor + private func appendResultMessages( + for result: QueryResultSet, + originalSQL: String, + databaseType: DatabaseType, + state: QueryEditorState + ) { + let isMessageOnlyStatement = QueryStatementClassifier.isLikelyMessageOnlyStatement( + originalSQL, + databaseType: databaseType + ) + + var emittedServerResponse = false + for serverMsg in result.serverMessages { + emittedServerResponse = true + let severity: QueryExecutionMessage.Severity = serverMsg.kind == .error ? .error : .info + var metadata = serverMsg.metadata + if serverMsg.number != 0 { + metadata["messageNumber"] = "\(serverMsg.number)" + } + if let serverName = serverMsg.serverName, !serverName.isEmpty { + metadata["server"] = serverName + } + state.appendMessage( + message: serverMsg.message, + severity: severity, + category: serverMsg.category ?? "Server Response", + procedure: serverMsg.procedureName, + line: serverMsg.lineNumber.map(Int.init), + metadata: metadata + ) + } + + if isMessageOnlyStatement, + let commandTag = result.commandTag?.trimmingCharacters(in: .whitespacesAndNewlines), + !commandTag.isEmpty { + emittedServerResponse = true + state.appendMessage( + message: commandTag, + severity: .info, + category: "Server Response", + metadata: ["commandTag": commandTag] + ) + } + + if isMessageOnlyStatement { + if !emittedServerResponse { + state.appendMessage( + message: "No additional server details were returned", + severity: .info, + category: "Server Response" + ) + } + return + } + + var metadata: [String: String] = [ + "rows": "\(result.rows.count)" + ] + let columnNames = result.columns.map(\.name).joined(separator: ", ") + if !columnNames.isEmpty { + metadata["columns"] = columnNames + } + if let commandTag = result.commandTag, !commandTag.isEmpty { + metadata["commandTag"] = commandTag + } + + state.appendMessage( + message: "Returned \(result.rows.count) row\(result.rows.count == 1 ? "" : "s")", + severity: .info, + metadata: metadata + ) + } } diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/Breadcrumbs/BreadcrumbToolbarContent+ConnectMenu.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/Breadcrumbs/BreadcrumbToolbarContent+ConnectMenu.swift index 886137c6c..b9d558bb7 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/Breadcrumbs/BreadcrumbToolbarContent+ConnectMenu.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/Breadcrumbs/BreadcrumbToolbarContent+ConnectMenu.swift @@ -65,8 +65,7 @@ final class ConnectToolbarMenuDelegate: NSObject { item.target = self item.representedObject = session item.state = isActive ? .on : .off - if let image = NSImage(named: conn.databaseType.iconName) { - image.size = NSSize(width: 16, height: 16) + if let image = conn.databaseType.menuIconImage() { item.image = image } else { item.image = NSImage(systemSymbolName: "server.rack", accessibilityDescription: nil) @@ -89,8 +88,7 @@ final class ConnectToolbarMenuDelegate: NSObject { item.target = self item.representedObject = conn item.state = .off - if let image = NSImage(named: conn.databaseType.iconName) { - image.size = NSSize(width: 16, height: 16) + if let image = conn.databaseType.menuIconImage() { item.image = image } else { item.image = NSImage(systemSymbolName: "server.rack", accessibilityDescription: nil) diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/Breadcrumbs/BreadcrumbToolbarContent+ConnectionsMenu.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/Breadcrumbs/BreadcrumbToolbarContent+ConnectionsMenu.swift index 3d6a9b109..a508d4de1 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/Breadcrumbs/BreadcrumbToolbarContent+ConnectionsMenu.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/Breadcrumbs/BreadcrumbToolbarContent+ConnectionsMenu.swift @@ -70,8 +70,7 @@ final class ConnectionsMenuDelegate: NSObject, NSMenuDelegate { item.target = self item.representedObject = session item.state = isActive ? .on : .off - if let image = NSImage(named: conn.databaseType.iconName) { - image.size = NSSize(width: 16, height: 16) + if let image = conn.databaseType.menuIconImage() { item.image = image } else { item.image = NSImage(systemSymbolName: "server.rack", accessibilityDescription: nil) @@ -122,8 +121,7 @@ final class ConnectionsMenuDelegate: NSObject, NSMenuDelegate { item.target = self item.representedObject = conn item.state = .off - if let image = NSImage(named: conn.databaseType.iconName) { - image.size = NSSize(width: 16, height: 16) + if let image = conn.databaseType.menuIconImage() { item.image = image } else { item.image = NSImage(systemSymbolName: "server.rack", accessibilityDescription: nil) diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/ConnectionToolbarMenuItems.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/ConnectionToolbarMenuItems.swift index ce6d5b3b0..a27858552 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/ConnectionToolbarMenuItems.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/ConnectionToolbarMenuItems.swift @@ -75,7 +75,7 @@ struct ConnectionToolbarMenuItems: View { private func connectionIcon(for connection: SavedConnection) -> ToolbarIcon { let assetName = connection.databaseType.iconName if hasImage(named: assetName) { - return .asset(assetName, isTemplate: false) + return .asset(assetName, isTemplate: connection.databaseType.usesTemplateIcon) } return .system("externaldrive") } diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsMenuBuilder.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsMenuBuilder.swift index 041f80031..c5891d321 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsMenuBuilder.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsMenuBuilder.swift @@ -100,12 +100,8 @@ enum ConnectionsMenuBuilder { item.state = .on } - if let image = NSImage(named: connection.databaseType.iconName) { - let sized = NSImage(size: NSSize(width: 16, height: 16), flipped: false) { rect in - image.draw(in: rect) - return true - } - item.image = sized + if let image = connection.databaseType.menuIconImage() { + item.image = image } else { item.image = NSImage(systemSymbolName: "server.rack", accessibilityDescription: nil)? .withSymbolConfiguration(.init(pointSize: 12, weight: .regular)) diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsPopoverController.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsPopoverController.swift index e1784a605..7f81c7af0 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsPopoverController.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsPopoverController.swift @@ -19,7 +19,7 @@ struct ConnectionsPopoverContent: View { PopoverConnectionRow( connection: conn, isSelected: conn.id == connectionStore.selectedConnectionID, - icon: iconImage(for: conn), + databaseType: conn.databaseType, displayName: displayName(for: conn) ) { environmentState.connect(to: conn) @@ -35,7 +35,7 @@ struct ConnectionsPopoverContent: View { PopoverConnectionRow( connection: conn, isSelected: conn.id == connectionStore.selectedConnectionID, - icon: iconImage(for: conn), + databaseType: conn.databaseType, displayName: displayName(for: conn) ) { environmentState.connect(to: conn) @@ -52,7 +52,7 @@ struct ConnectionsPopoverContent: View { PopoverConnectionRow( connection: conn, isSelected: conn.id == connectionStore.selectedConnectionID, - icon: iconImage(for: conn), + databaseType: conn.databaseType, displayName: displayName(for: conn) ) { environmentState.connect(to: conn) @@ -127,10 +127,6 @@ struct ConnectionsPopoverContent: View { .padding(.vertical, SpacingTokens.xxs2) } - private func iconImage(for connection: SavedConnection) -> NSImage? { - NSImage(named: connection.databaseType.iconName) - } - private func displayName(for connection: SavedConnection) -> String { let trimmed = connection.connectionName.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? connection.host : trimmed @@ -142,7 +138,7 @@ struct ConnectionsPopoverContent: View { private struct PopoverConnectionRow: View { let connection: SavedConnection let isSelected: Bool - let icon: NSImage? + let databaseType: DatabaseType let displayName: String let action: () -> Void @@ -156,17 +152,7 @@ private struct PopoverConnectionRow: View { .frame(width: 14) .opacity(isSelected ? 1 : 0) - if let icon { - Image(nsImage: icon) - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - } else { - Image(systemName: "server.rack") - .font(TypographyTokens.caption2) - .foregroundStyle(ColorTokens.Text.secondary) - .frame(width: 16, height: 16) - } + DatabaseTypeIcon(databaseType: databaseType, presentation: .menu) Text(displayName) .font(TypographyTokens.standard) diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsPopoverView.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsPopoverView.swift index a71a39d9f..1e6f9b286 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsPopoverView.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/Popovers/ConnectionsPopoverView.swift @@ -121,15 +121,7 @@ struct ConnectionsPopoverView: View { @ViewBuilder private func connectionIcon(_ connection: SavedConnection) -> some View { - if let image = NSImage(named: connection.databaseType.iconName) { - Image(nsImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - } else { - Image(systemName: "server.rack") - .font(TypographyTokens.caption2) - .foregroundStyle(ColorTokens.Text.secondary) - } + DatabaseTypeIcon(databaseType: connection.databaseType, presentation: .menu) } // MARK: - Data diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/TableStructureToolbarItem.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/TableStructureToolbarItem.swift new file mode 100644 index 000000000..ff4eaadc8 --- /dev/null +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/TableStructureToolbarItem.swift @@ -0,0 +1,155 @@ +import SwiftUI + +/// Toolbar controls for Structure tab — shown only when active tab is a Structure editor. +/// Provides Add, Script, and Apply buttons with dynamic visibility based on pending changes. +struct TableStructureToolbarItem: View { + private struct ApplyReviewPresentation: Identifiable { + let id = UUID() + let tableName: String + let statements: [String] + } + + @Environment(TabStore.self) private var tabStore + @Environment(EnvironmentState.self) private var environmentState + @Environment(AppState.self) private var appState + @State private var applyReview: ApplyReviewPresentation? + + var body: some View { + if let tab = tabStore.activeTab, let vm = tab.structureEditor { + structureControls(viewModel: vm, tab: tab) + } else { + EmptyView() + } + } + + @ViewBuilder + private func structureControls(viewModel: TableStructureEditorViewModel, tab: WorkspaceTab) -> some View { + HStack(spacing: SpacingTokens.sm) { + addButton + .glassEffect(.regular.interactive()) + + if viewModel.hasPendingChanges { + HStack(spacing: SpacingTokens.none) { + scriptButton(viewModel: viewModel) + applyButton(viewModel: viewModel, tab: tab) + } + .glassEffect(.regular.interactive()) + } + } + .sheet(item: $applyReview) { review in + StructureApplyReviewSheet( + tableName: review.tableName, + statements: review.statements + ) { + await applyChanges(viewModel: viewModel, tab: tab) + } + } + } + + private var addButton: some View { + Menu { + Button { + if let vm = tabStore.activeTab?.structureEditor { + vm.requestAddAction(.column, section: .columns) + } + } label: { + Label("Add Column", systemImage: "tablecells") + } + + Button { + if let vm = tabStore.activeTab?.structureEditor { + vm.requestAddAction(.index, section: .indexes) + } + } label: { + Label("Add Index", systemImage: "list.bullet.rectangle") + } + + Divider() + + Button { + if let vm = tabStore.activeTab?.structureEditor { + vm.requestAddAction(.foreignKey, section: .relations) + } + } label: { + Label("Add Foreign Key", systemImage: "link") + } + + Button { + if let vm = tabStore.activeTab?.structureEditor { + vm.requestAddAction(.uniqueConstraint, section: .constraints) + } + } label: { + Label("Add Unique Constraint", systemImage: "checkmark.shield") + } + + Button { + if let vm = tabStore.activeTab?.structureEditor { + vm.requestAddAction(.checkConstraint, section: .constraints) + } + } label: { + Label("Add Check Constraint", systemImage: "checkmark.rectangle.stack") + } + } label: { + Label("Add", systemImage: "plus") + .labelStyle(.iconOnly) + } + .menuStyle(.button) + .menuIndicator(.hidden) + .help("Add item to table structure") + .accessibilityLabel("Add") + } + + private func scriptButton(viewModel: TableStructureEditorViewModel) -> some View { + Button { + let statements = viewModel.generateStatements() + guard !statements.isEmpty else { return } + appState.showStructureScriptPreview(statements: statements) + } label: { + Label("Script", systemImage: "doc.text") + } + .labelStyle(.iconOnly) + .help("View generated SQL script") + .accessibilityLabel("View script") + .disabled(viewModel.isApplying) + } + + private func applyButton(viewModel: TableStructureEditorViewModel, tab: WorkspaceTab) -> some View { + Button { + let statements = viewModel.generateStatements() + guard !statements.isEmpty else { return } + applyReview = ApplyReviewPresentation(tableName: viewModel.tableName, statements: statements) + } label: { + Label("Apply", systemImage: "checkmark") + } + .labelStyle(.iconOnly) + .help("Apply changes to database") + .accessibilityLabel("Apply changes") + .disabled(viewModel.isApplying) + .keyboardShortcut(.return, modifiers: [.command, .shift]) + } + + private func applyChanges(viewModel: TableStructureEditorViewModel, tab: WorkspaceTab) async -> Bool { + await viewModel.applyChanges() + + if let error = viewModel.lastError { + environmentState.notificationEngine?.post( + category: .generalError, + message: error + ) + return false + } else if viewModel.lastSuccessMessage != nil { + environmentState.notificationEngine?.post( + category: .generalSuccess, + message: "Structure of \(viewModel.tableName) updated" + ) + await environmentState.refreshDatabaseStructure( + for: tab.connectionSessionID, + scope: .selectedDatabase, + databaseOverride: tab.connection.database.isEmpty ? nil : tab.connection.database + ) + return true + } + + return false + } +} diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+Connections.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+Connections.swift index 3d4535225..615e7b040 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+Connections.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+Connections.swift @@ -78,13 +78,10 @@ struct ConnectionsMenuButton: View { Button { environmentState.connectToNewSession(to: connection) } label: { - HStack { - if let image = NSImage(named: connection.databaseType.iconName) { - Image(nsImage: image) - } else { - Image(systemName: "server.rack") - } + Label { Text(connection.connectionName.isEmpty ? connection.host : connection.connectionName) + } icon: { + DatabaseTypeIcon(databaseType: connection.databaseType, presentation: .menu) } } } @@ -99,11 +96,7 @@ struct ConnectionsMenuButton: View { environmentState.sessionGroup.setActiveSession(session.id) } label: { HStack { - if let image = NSImage(named: conn.databaseType.iconName) { - Image(nsImage: image) - } else { - Image(systemName: "server.rack") - } + DatabaseTypeIcon(databaseType: conn.databaseType, presentation: .menu) Text(session.displayName) if isActive { Spacer() @@ -152,13 +145,10 @@ struct ToolbarFolderMenu: View { Button { onConnect(connection) } label: { - HStack { - if let image = NSImage(named: connection.databaseType.iconName) { - Image(nsImage: image) - } else { - Image(systemName: "server.rack") - } + Label { Text(connection.connectionName.isEmpty ? connection.host : connection.connectionName) + } icon: { + DatabaseTypeIcon(databaseType: connection.databaseType, presentation: .menu) } } } diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+PreviewData.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+PreviewData.swift index b34d26cd7..33a61ba61 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+PreviewData.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+PreviewData.swift @@ -39,6 +39,12 @@ struct WorkspaceToolbarPreviewData { )) let navigationStore = NavigationStore() let tabStore = TabStore() + let objectBrowserCacheStore = ObjectBrowserCacheStore( + configuration: .init( + rootDirectory: FileManager.default.temporaryDirectory + .appendingPathComponent("EchoPreviewObjectBrowserCache", isDirectory: true) + ) + ) let environmentState = EnvironmentState( projectStore: projectStore, @@ -49,12 +55,17 @@ struct WorkspaceToolbarPreviewData { resultSpoolConfigCoordinator: ResultSpoolConfig(spoolManager: spoolManager), diagramBuilder: DiagramBuilder(cacheManager: diagramManager, keyStore: diagramKeyStore), identityRepository: IdentityRepository(connectionStore: connectionStore), - schemaDiscoveryEngine: MetadataDiscoveryEngine(identityRepository: IdentityRepository(connectionStore: connectionStore), connectionStore: connectionStore), + schemaDiscoveryEngine: MetadataDiscoveryEngine( + identityRepository: IdentityRepository(connectionStore: connectionStore), + connectionStore: connectionStore, + objectBrowserCacheStore: objectBrowserCacheStore + ), bookmarkRepository: BookmarkRepository(), historyRepository: HistoryRepository(), resultSpoolManager: spoolManager, diagramCacheStore: diagramManager, - diagramKeyStore: diagramKeyStore + diagramKeyStore: diagramKeyStore, + objectBrowserCacheStore: objectBrowserCacheStore ) let appState = AppState() let appearanceStore = AppearanceStore.shared diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+Recents.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+Recents.swift index 87d972bff..17f5cd2f3 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+Recents.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems+Recents.swift @@ -26,11 +26,7 @@ struct RecentConnectionsMenuButton: View { } } label: { HStack { - if let image = NSImage(named: record.databaseType.iconName) { - Image(nsImage: image) - } else { - Image(systemName: "server.rack") - } + DatabaseTypeIcon(databaseType: record.databaseType, presentation: .menu) let baseName = record.connectionName.isEmpty ? record.host : record.connectionName diff --git a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems.swift b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems.swift index ec1ece447..55431ee78 100644 --- a/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems.swift +++ b/Echo/Sources/Features/AppHost/Views/Toolbar/WorkspaceToolbarItems/WorkspaceToolbarItems.swift @@ -57,6 +57,12 @@ struct WorkspaceToolbarItems: CustomizableToolbarContent { @ToolbarContentBuilder private var contextActionItems: some CustomizableToolbarContent { + // Structure tab — Add/Script/Apply buttons + ToolbarItem(id: "workspace.primary.structure", placement: .primaryAction) { + TableStructureToolbarItem() + } + .sharedBackgroundVisibility(.hidden) + // Activity Monitor, Job Queue, Maintenance — tab-specific controls ToolbarItem(id: "workspace.primary.activitymonitor", placement: .primaryAction) { ActivityMonitorToolbarItem() diff --git a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+MetadataFreshness.swift b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+MetadataFreshness.swift new file mode 100644 index 000000000..043fb776a --- /dev/null +++ b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+MetadataFreshness.swift @@ -0,0 +1,99 @@ +import Foundation + +enum DatabaseMetadataFreshness: String, Codable, Sendable { + case listOnly + case cached + case refreshing + case live + case failed +} + +extension ConnectionSession { + func isRefreshingMetadata(forDatabase databaseName: String) -> Bool { + metadataFreshness(forDatabase: databaseName) == .refreshing + || schemaLoadsInFlight.contains(schemaLoadKey(databaseName)) + } + + func metadataFreshness(forDatabase databaseName: String) -> DatabaseMetadataFreshness { + metadataFreshnessByDatabase[schemaLoadKey(databaseName)] ?? .listOnly + } + + func hydrateMetadataFreshnessFromCacheStructure() { + guard let structure = databaseStructure else { + metadataFreshnessByDatabase.removeAll() + return + } + metadataFreshnessByDatabase = Self.makeMetadataFreshnessMap( + from: structure, + loadedState: .cached, + preserveExisting: false, + existing: [:] + ) + } + + func reconcileMetadataFreshnessFromLiveStructure( + markingLive databasesLoadedLive: Set = [] + ) { + guard let structure = databaseStructure else { + metadataFreshnessByDatabase.removeAll() + return + } + let normalizedLive = Set(databasesLoadedLive.map(schemaLoadKey)) + metadataFreshnessByDatabase = Self.makeMetadataFreshnessMap( + from: structure, + loadedState: .cached, + preserveExisting: true, + existing: metadataFreshnessByDatabase, + liveDatabases: normalizedLive + ) + } + + func markMetadataRefreshStarted(forDatabase databaseName: String) { + metadataFreshnessByDatabase[schemaLoadKey(databaseName)] = .refreshing + } + + func markMetadataRefreshCompleted(forDatabase databaseName: String, hasSchemas: Bool) { + metadataFreshnessByDatabase[schemaLoadKey(databaseName)] = hasSchemas ? .live : .listOnly + } + + func markMetadataRefreshFailed(forDatabase databaseName: String) { + metadataFreshnessByDatabase[schemaLoadKey(databaseName)] = .failed + } + + func clearMetadataCacheState() { + metadataFreshnessByDatabase.removeAll() + databaseStructure = nil + structureLoadingState = .idle + structureLoadingMessage = nil + schemaLoadsInFlight.removeAll() + } + + private static func makeMetadataFreshnessMap( + from structure: DatabaseStructure, + loadedState: DatabaseMetadataFreshness, + preserveExisting: Bool, + existing: [String: DatabaseMetadataFreshness], + liveDatabases: Set = [] + ) -> [String: DatabaseMetadataFreshness] { + var next: [String: DatabaseMetadataFreshness] = [:] + for database in structure.databases { + let key = database.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + guard !key.isEmpty else { continue } + let hasSchemas = database.schemas.contains(where: { !$0.objects.isEmpty }) + if !hasSchemas { + next[key] = .listOnly + continue + } + if liveDatabases.contains(key) { + next[key] = .live + continue + } + if preserveExisting, let state = existing[key], state == .live || state == .refreshing { + next[key] = .live + continue + } + next[key] = loadedState + } + return next + } +} diff --git a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+SchemaLoading.swift b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+SchemaLoading.swift index 964665a06..8c2933cbc 100644 --- a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+SchemaLoading.swift +++ b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+SchemaLoading.swift @@ -16,9 +16,12 @@ extension ConnectionSession { func hasLoadedSchema(forDatabase databaseName: String) -> Bool { let normalizedName = normalizedDatabaseName(databaseName) guard !normalizedName.isEmpty else { return false } + let key = schemaLoadKey(normalizedName) + if metadataFreshnessByDatabase[key] == .listOnly { + return false + } return databaseStructure?.databases - .first(where: { normalizedDatabaseName($0.name).caseInsensitiveCompare(normalizedName) == .orderedSame })? - .schemas.isEmpty == false + .first(where: { normalizedDatabaseName($0.name).caseInsensitiveCompare(normalizedName) == .orderedSame }) != nil } func beginSchemaLoad(forDatabase databaseName: String) -> Bool { diff --git a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Monitor.swift b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Monitor.swift new file mode 100644 index 000000000..8fe689492 --- /dev/null +++ b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Monitor.swift @@ -0,0 +1,144 @@ +import Foundation +import SwiftUI +import SQLServerKit + +// MARK: - Monitor Tab Factory Methods + +extension ConnectionSession { + + /// Display label for the server in tab subtitles — prefers connection name, falls back to host. + var serverLabel: String { + let connName = connection.connectionName.trimmingCharacters(in: .whitespacesAndNewlines) + return connName.isEmpty ? connection.host : connName + } + + @discardableResult + func addActivityMonitorTab() throws -> WorkspaceTab { + // Reuse existing activity monitor tab if present + if let existing = queryTabs.first(where: { $0.activityMonitor != nil }) { + activeQueryTabID = existing.id + return existing + } + + let monitor = try session.makeActivityMonitor() + let interval = AppDirector.shared.projectStore.globalSettings.activityMonitorRefreshInterval + let viewModel = ActivityMonitorViewModel( + monitor: monitor, + mysqlSession: session as? MySQLSession, + connectionSessionID: self.id, + connectionID: connection.id, + databaseType: connection.databaseType, + refreshInterval: interval + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + + if let mssql = session as? MSSQLSession { + let xeVM = ExtendedEventsViewModel( + xeClient: mssql.extendedEvents, + connectionSessionID: id + ) + xeVM.activityEngine = AppDirector.shared.activityEngine + viewModel.extendedEventsVM = xeVM + viewModel.profilerVM = ProfilerViewModel( + profilerClient: mssql.profiler, + session: session, + connectionSessionID: id + ) + } + + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Activity Monitor", + content: .activityMonitor(viewModel), + activeDatabaseName: nil + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + func addExtendedEventsTab() -> WorkspaceTab? { + guard let mssql = session as? MSSQLSession else { return nil } + + // Reuse existing extended events tab if present + if let existing = queryTabs.first(where: { $0.extendedEventsVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = ExtendedEventsViewModel( + xeClient: mssql.extendedEvents, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Extended Events", + content: .extendedEvents(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + func addProfilerTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.profilerVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = ProfilerViewModel( + profilerClient: session.profiler, + session: session, + connectionSessionID: id + ) + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "SQL Profiler", + content: .profiler(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + func addResourceGovernorTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.resourceGovernorVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = ResourceGovernorViewModel( + rgClient: session.resourceGovernor, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Resource Governor", + content: .resourceGovernor(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } +} diff --git a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Security.swift b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Security.swift new file mode 100644 index 000000000..d289eb4cd --- /dev/null +++ b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Security.swift @@ -0,0 +1,142 @@ +import Foundation +import SwiftUI + +// MARK: - Security Tab Factory Methods + +extension ConnectionSession { + + @discardableResult + func addDatabaseSecurityTab(databaseName: String? = nil) -> WorkspaceTab { + if connection.databaseType == .postgresql { + return addPostgresDatabaseSecurityTab() + } + if connection.databaseType == .mysql { + return addMySQLDatabaseSecurityTab(databaseName: databaseName) + } + return addMSSQLDatabaseSecurityTab(databaseName: databaseName) + } + + @discardableResult + private func addMSSQLDatabaseSecurityTab(databaseName: String? = nil) -> WorkspaceTab { + let effectiveDatabase = databaseName ?? sidebarFocusedDatabase ?? connection.database + + if let existing = queryTabs.first(where: { $0.databaseSecurity != nil }) { + activeQueryTabID = existing.id + if let vm = existing.databaseSecurity, vm.selectedDatabase != effectiveDatabase { + existing.activeDatabaseName = effectiveDatabase.isEmpty ? nil : effectiveDatabase + Task { await vm.selectDatabase(effectiveDatabase) } + } + return existing + } + + let viewModel = DatabaseSecurityViewModel( + session: session, + connectionID: connection.id, + connectionSessionID: id, + initialDatabase: effectiveDatabase.isEmpty ? nil : effectiveDatabase + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + + let dbName = databaseName ?? sidebarFocusedDatabase + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Database Security", + content: .databaseSecurity(viewModel), + activeDatabaseName: (dbName?.isEmpty == false) ? dbName : nil + ) + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + private func addPostgresDatabaseSecurityTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.postgresSecurity != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = PostgresDatabaseSecurityViewModel( + session: session, + connectionID: connection.id, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Database Security", + content: .postgresSecurity(viewModel) + ) + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + private func addMySQLDatabaseSecurityTab(databaseName: String? = nil) -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.mysqlSecurity != nil }) { + activeQueryTabID = existing.id + if let databaseName, !databaseName.isEmpty { + existing.activeDatabaseName = databaseName + } + return existing + } + + let viewModel = MySQLDatabaseSecurityViewModel( + session: session, + connectionID: connection.id, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + + let effectiveDatabase = databaseName ?? sidebarFocusedDatabase ?? connection.database + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Database Security", + content: .mysqlSecurity(viewModel), + activeDatabaseName: effectiveDatabase.isEmpty ? nil : effectiveDatabase + ) + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + func addServerSecurityTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.serverSecurity != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = ServerSecurityViewModel( + session: session, + connectionID: connection.id, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Server Security", + content: .serverSecurity(viewModel), + activeDatabaseName: nil + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } +} diff --git a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+ServerTools.swift b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+ServerTools.swift new file mode 100644 index 000000000..2a97d904b --- /dev/null +++ b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+ServerTools.swift @@ -0,0 +1,264 @@ +import Foundation +import SwiftUI +import SQLServerKit + +// MARK: - Server Tools Tab Factory Methods + +extension ConnectionSession { + + @discardableResult + func addServerPropertiesTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.serverPropertiesVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = ServerPropertiesViewModel( + session: session, + connectionID: connection.id, + connectionSessionID: id, + connectionHost: connection.host + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Server Properties", + content: .serverProperties(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + func addTuningAdvisorTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.tuningAdvisorVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = TuningAdvisorViewModel( + tuningClient: session.tuning, + session: session, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Tuning Advisor", + content: .tuningAdvisor(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + func addPolicyManagementTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.policyManagementVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = PolicyManagementViewModel( + policyClient: session.policy, + connectionSessionID: id + ) + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Policy Management", + content: .policyManagement(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + @discardableResult + func addAvailabilityGroupsTab() -> WorkspaceTab? { + guard let mssql = session as? MSSQLSession else { return nil } + + // Reuse existing availability groups tab if present + if let existing = queryTabs.first(where: { $0.availabilityGroupsVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = AvailabilityGroupsViewModel( + agClient: mssql.availabilityGroups, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Availability Groups", + content: .availabilityGroups(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + // MARK: - Error Log Tab + + @discardableResult + func addErrorLogTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.errorLogVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = ErrorLogViewModel(session: session, connectionSessionID: id) + viewModel.activityEngine = AppDirector.shared.activityEngine + viewModel.notificationEngine = AppDirector.shared.notificationEngine + + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Error Log", + content: .errorLog(viewModel), + activeDatabaseName: nil + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + // MARK: - Advanced Objects Tab (PostgreSQL) + + @discardableResult + func addPostgresAdvancedObjectsTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.postgresAdvancedObjectsVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = PostgresAdvancedObjectsViewModel( + session: session, + connectionID: connection.id, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Advanced Objects", + content: .postgresAdvancedObjects(viewModel), + activeDatabaseName: nil + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + // MARK: - Advanced Objects Tab (MSSQL) + + @discardableResult + func addMSSQLAdvancedObjectsTab(databaseName: String) -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.mssqlAdvancedObjectsVM != nil }) { + if let vm = existing.mssqlAdvancedObjectsVM, vm.databaseName != databaseName { + vm.databaseName = databaseName + vm.isInitialized = false + Task { await vm.initialize() } + } + activeQueryTabID = existing.id + return existing + } + + let viewModel = MSSQLAdvancedObjectsViewModel( + session: session, + connectionID: connection.id, + connectionSessionID: id, + databaseName: databaseName + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Advanced Objects", + content: .mssqlAdvancedObjects(viewModel), + activeDatabaseName: databaseName + ) + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + // MARK: - Schema Diff Tab (PostgreSQL) + + @discardableResult + func addSchemaDiffTab() -> WorkspaceTab { + if let existing = queryTabs.first(where: { $0.schemaDiffVM != nil }) { + activeQueryTabID = existing.id + return existing + } + + let viewModel = SchemaDiffViewModel( + session: session, + connectionID: connection.id, + connectionSessionID: id + ) + viewModel.activityEngine = AppDirector.shared.activityEngine + + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Schema Diff", + content: .schemaDiff(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } + + // MARK: - Visual Query Builder + + @discardableResult + func addQueryBuilderTab() -> WorkspaceTab { + let viewModel = VisualQueryBuilderViewModel( + databaseType: connection.databaseType, + session: session + ) + let tab = WorkspaceTab( + connection: connection, + session: session, + connectionSessionID: id, + title: "Query Builder", + content: .queryBuilder(viewModel) + ) + tab.tabSubtitle = serverLabel + queryTabs.append(tab) + activeQueryTabID = tab.id + lastActivity = Date() + return tab + } +} diff --git a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Specialized.swift b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Specialized.swift deleted file mode 100644 index c164af2d3..000000000 --- a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession+TabFactory+Specialized.swift +++ /dev/null @@ -1,536 +0,0 @@ -import Foundation -import SwiftUI -import SQLServerKit - -// MARK: - Specialized Tab Factory Methods (Monitor, MSSQL, Security) - -extension ConnectionSession { - - /// Display label for the server in tab subtitles — prefers connection name, falls back to host. - private var serverLabel: String { - let connName = connection.connectionName.trimmingCharacters(in: .whitespacesAndNewlines) - return connName.isEmpty ? connection.host : connName - } - - @discardableResult - func addActivityMonitorTab() throws -> WorkspaceTab { - // Reuse existing activity monitor tab if present - if let existing = queryTabs.first(where: { $0.activityMonitor != nil }) { - activeQueryTabID = existing.id - return existing - } - - let monitor = try session.makeActivityMonitor() - let interval = AppDirector.shared.projectStore.globalSettings.activityMonitorRefreshInterval - let viewModel = ActivityMonitorViewModel( - monitor: monitor, - mysqlSession: session as? MySQLSession, - connectionSessionID: self.id, - connectionID: connection.id, - databaseType: connection.databaseType, - refreshInterval: interval - ) - - if let mssql = session as? MSSQLSession { - let xeVM = ExtendedEventsViewModel( - xeClient: mssql.extendedEvents, - connectionSessionID: id - ) - xeVM.activityEngine = AppDirector.shared.activityEngine - viewModel.extendedEventsVM = xeVM - viewModel.profilerVM = ProfilerViewModel( - profilerClient: mssql.profiler, - session: session, - connectionSessionID: id - ) - } - - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Activity Monitor", - content: .activityMonitor(viewModel), - activeDatabaseName: nil - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - func addExtendedEventsTab() -> WorkspaceTab? { - guard let mssql = session as? MSSQLSession else { return nil } - - // Reuse existing extended events tab if present - if let existing = queryTabs.first(where: { $0.extendedEventsVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = ExtendedEventsViewModel( - xeClient: mssql.extendedEvents, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Extended Events", - content: .extendedEvents(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - func addProfilerTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.profilerVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = ProfilerViewModel( - profilerClient: session.profiler, - session: session, - connectionSessionID: id - ) - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "SQL Profiler", - content: .profiler(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - func addResourceGovernorTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.resourceGovernorVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = ResourceGovernorViewModel( - rgClient: session.resourceGovernor, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Resource Governor", - content: .resourceGovernor(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - func addServerPropertiesTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.serverPropertiesVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = ServerPropertiesViewModel( - session: session, - connectionID: connection.id, - connectionSessionID: id, - connectionHost: connection.host - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Server Properties", - content: .serverProperties(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - func addTuningAdvisorTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.tuningAdvisorVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = TuningAdvisorViewModel( - tuningClient: session.tuning, - session: session, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Tuning Advisor", - content: .tuningAdvisor(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - func addPolicyManagementTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.policyManagementVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = PolicyManagementViewModel( - policyClient: session.policy, - connectionSessionID: id - ) - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Policy Management", - content: .policyManagement(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - func addAvailabilityGroupsTab() -> WorkspaceTab? { - guard let mssql = session as? MSSQLSession else { return nil } - - // Reuse existing availability groups tab if present - if let existing = queryTabs.first(where: { $0.availabilityGroupsVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = AvailabilityGroupsViewModel( - agClient: mssql.availabilityGroups, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Availability Groups", - content: .availabilityGroups(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - // MARK: - Error Log Tab - - @discardableResult - func addErrorLogTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.errorLogVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = ErrorLogViewModel(session: session, connectionSessionID: id) - viewModel.activityEngine = AppDirector.shared.activityEngine - viewModel.notificationEngine = AppDirector.shared.notificationEngine - - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Error Log", - content: .errorLog(viewModel), - activeDatabaseName: nil - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - // MARK: - Advanced Objects Tab (PostgreSQL) - - @discardableResult - func addPostgresAdvancedObjectsTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.postgresAdvancedObjectsVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = PostgresAdvancedObjectsViewModel( - session: session, - connectionID: connection.id, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Advanced Objects", - content: .postgresAdvancedObjects(viewModel), - activeDatabaseName: nil - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - // MARK: - Advanced Objects Tab (MSSQL) - - @discardableResult - func addMSSQLAdvancedObjectsTab(databaseName: String) -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.mssqlAdvancedObjectsVM != nil }) { - if let vm = existing.mssqlAdvancedObjectsVM, vm.databaseName != databaseName { - vm.databaseName = databaseName - vm.isInitialized = false - Task { await vm.initialize() } - } - activeQueryTabID = existing.id - return existing - } - - let viewModel = MSSQLAdvancedObjectsViewModel( - session: session, - connectionID: connection.id, - connectionSessionID: id, - databaseName: databaseName - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Advanced Objects", - content: .mssqlAdvancedObjects(viewModel), - activeDatabaseName: databaseName - ) - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - // MARK: - Schema Diff Tab (PostgreSQL) - - @discardableResult - func addSchemaDiffTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.schemaDiffVM != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = SchemaDiffViewModel( - session: session, - connectionID: connection.id, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Schema Diff", - content: .schemaDiff(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - // MARK: - Visual Query Builder - - @discardableResult - func addQueryBuilderTab() -> WorkspaceTab { - let viewModel = VisualQueryBuilderViewModel( - databaseType: connection.databaseType, - session: session - ) - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Query Builder", - content: .queryBuilder(viewModel) - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - // MARK: - Security Tabs - - @discardableResult - func addDatabaseSecurityTab(databaseName: String? = nil) -> WorkspaceTab { - if connection.databaseType == .postgresql { - return addPostgresDatabaseSecurityTab() - } - if connection.databaseType == .mysql { - return addMySQLDatabaseSecurityTab(databaseName: databaseName) - } - return addMSSQLDatabaseSecurityTab(databaseName: databaseName) - } - - @discardableResult - private func addMSSQLDatabaseSecurityTab(databaseName: String? = nil) -> WorkspaceTab { - let effectiveDatabase = databaseName ?? sidebarFocusedDatabase ?? connection.database - - if let existing = queryTabs.first(where: { $0.databaseSecurity != nil }) { - activeQueryTabID = existing.id - if let vm = existing.databaseSecurity, vm.selectedDatabase != effectiveDatabase { - existing.activeDatabaseName = effectiveDatabase.isEmpty ? nil : effectiveDatabase - Task { await vm.selectDatabase(effectiveDatabase) } - } - return existing - } - - let viewModel = DatabaseSecurityViewModel( - session: session, - connectionID: connection.id, - connectionSessionID: id, - initialDatabase: effectiveDatabase.isEmpty ? nil : effectiveDatabase - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - - let dbName = databaseName ?? sidebarFocusedDatabase - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Database Security", - content: .databaseSecurity(viewModel), - activeDatabaseName: (dbName?.isEmpty == false) ? dbName : nil - ) - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - private func addPostgresDatabaseSecurityTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.postgresSecurity != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = PostgresDatabaseSecurityViewModel( - session: session, - connectionID: connection.id, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Database Security", - content: .postgresSecurity(viewModel) - ) - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - private func addMySQLDatabaseSecurityTab(databaseName: String? = nil) -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.mysqlSecurity != nil }) { - activeQueryTabID = existing.id - if let databaseName, !databaseName.isEmpty { - existing.activeDatabaseName = databaseName - } - return existing - } - - let viewModel = MySQLDatabaseSecurityViewModel( - session: session, - connectionID: connection.id, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - - let effectiveDatabase = databaseName ?? sidebarFocusedDatabase ?? connection.database - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Database Security", - content: .mysqlSecurity(viewModel), - activeDatabaseName: effectiveDatabase.isEmpty ? nil : effectiveDatabase - ) - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } - - @discardableResult - func addServerSecurityTab() -> WorkspaceTab { - if let existing = queryTabs.first(where: { $0.serverSecurity != nil }) { - activeQueryTabID = existing.id - return existing - } - - let viewModel = ServerSecurityViewModel( - session: session, - connectionID: connection.id, - connectionSessionID: id - ) - viewModel.activityEngine = AppDirector.shared.activityEngine - - let tab = WorkspaceTab( - connection: connection, - session: session, - connectionSessionID: id, - title: "Server Security", - content: .serverSecurity(viewModel), - activeDatabaseName: nil - ) - tab.tabSubtitle = serverLabel - queryTabs.append(tab) - activeQueryTabID = tab.id - lastActivity = Date() - return tab - } -} diff --git a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession.swift b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession.swift index 91e02eeec..9c7efcc2c 100644 --- a/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession.swift +++ b/Echo/Sources/Features/ConnectionVault/Domain/ConnectionSession.swift @@ -45,6 +45,7 @@ final class ConnectionSession: Identifiable { @ObservationIgnored var defaultBackgroundStreamingThreshold: Int @ObservationIgnored var defaultBackgroundFetchSize: Int @ObservationIgnored var schemaLoadsInFlight: Set = [] + @ObservationIgnored var metadataFreshnessByDatabase: [String: DatabaseMetadataFreshness] = [:] // Query tabs specific to this connection var queryTabs: [WorkspaceTab] = [] diff --git a/Echo/Sources/Features/ConnectionVault/Domain/SavedConnection+ObjectBrowserCache.swift b/Echo/Sources/Features/ConnectionVault/Domain/SavedConnection+ObjectBrowserCache.swift new file mode 100644 index 000000000..9f69765fc --- /dev/null +++ b/Echo/Sources/Features/ConnectionVault/Domain/SavedConnection+ObjectBrowserCache.swift @@ -0,0 +1,21 @@ +import Foundation + +extension SavedConnection { + var objectBrowserCacheFingerprint: String { + [ + "type=\(databaseType.rawValue)", + "host=\(host.lowercased())", + "port=\(port)", + "database=\(database.lowercased())", + "username=\(username.lowercased())", + "auth=\(authenticationMethod.rawValue)", + "domain=\(domain.lowercased())", + "tls=\(useTLS)", + "trust=\(trustServerCertificate)", + "tlsMode=\(tlsMode.rawValue)", + "mssqlEnc=\(mssqlEncryptionMode.rawValue)", + "readonly=\(readOnlyIntent)" + ] + .joined(separator: "|") + } +} diff --git a/Echo/Sources/Features/ConnectionVault/Domain/SavedConnection.swift b/Echo/Sources/Features/ConnectionVault/Domain/SavedConnection.swift index 5906c1d4d..8892c11b3 100644 --- a/Echo/Sources/Features/ConnectionVault/Domain/SavedConnection.swift +++ b/Echo/Sources/Features/ConnectionVault/Domain/SavedConnection.swift @@ -18,10 +18,17 @@ enum DatabaseType: String, Sendable, Codable, CaseIterable { nonisolated var iconName: String { switch self { - case .postgresql: return "postgresql" - case .mysql: return "mysql" - case .microsoftSQL: return "mssql" - case .sqlite: return "sqlite" + case .postgresql: return "PostgreSQL" + case .mysql: return "MySQL" + case .microsoftSQL: return "MicrosoftSQLServer" + case .sqlite: return "SQLite" + } + } + + nonisolated var usesTemplateIcon: Bool { + switch self { + case .postgresql, .mysql, .microsoftSQL, .sqlite: + return false } } diff --git a/Echo/Sources/Features/ConnectionVault/Views/ConnectionEditor/ConnectionEditorView+Actions.swift b/Echo/Sources/Features/ConnectionVault/Views/ConnectionEditor/ConnectionEditorView+Actions.swift index 768c5ffaf..9712af3e3 100644 --- a/Echo/Sources/Features/ConnectionVault/Views/ConnectionEditor/ConnectionEditorView+Actions.swift +++ b/Echo/Sources/Features/ConnectionVault/Views/ConnectionEditor/ConnectionEditorView+Actions.swift @@ -107,9 +107,10 @@ extension ConnectionEditorView { iconImage.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0) - // Tint the icon with the color - NSColor(color).setFill() - iconRect.fill(using: .sourceAtop) + if databaseType.usesTemplateIcon { + NSColor(color).setFill() + iconRect.fill(using: .sourceAtop) + } } // Convert to PNG diff --git a/Echo/Sources/Features/ConnectionVault/Views/ConnectionEditor/ConnectionEditorView+Detail.swift b/Echo/Sources/Features/ConnectionVault/Views/ConnectionEditor/ConnectionEditorView+Detail.swift index b75d0c683..2529ea779 100644 --- a/Echo/Sources/Features/ConnectionVault/Views/ConnectionEditor/ConnectionEditorView+Detail.swift +++ b/Echo/Sources/Features/ConnectionVault/Views/ConnectionEditor/ConnectionEditorView+Detail.swift @@ -50,10 +50,7 @@ extension ConnectionEditorView { Label { Text(type.displayName) } icon: { - Image(type.iconName) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: SpacingTokens.md, height: SpacingTokens.md) + DatabaseTypeIcon(databaseType: type, presentation: .formControl) } .tag(type) } diff --git a/Echo/Sources/Features/ConnectionVault/Views/ManageConnections/Projects/ManageConnectionsView+Projects.swift b/Echo/Sources/Features/ConnectionVault/Views/ManageConnections/Projects/ManageConnectionsView+Projects.swift index dd2d3be71..dcb6468d0 100644 --- a/Echo/Sources/Features/ConnectionVault/Views/ManageConnections/Projects/ManageConnectionsView+Projects.swift +++ b/Echo/Sources/Features/ConnectionVault/Views/ManageConnections/Projects/ManageConnectionsView+Projects.swift @@ -252,7 +252,7 @@ extension ManageConnectionsView { Task { try? await projectStore.updateProject(updated) if newValue, let syncEngine = AppDirector.shared.syncEngine { - try? await syncEngine.performInitialUpload(for: updated) + try? await syncEngine.performInitialUpload(for: updated, strategy: .merge) } } } diff --git a/Echo/Sources/Features/ConnectionVault/Views/Sidebar/ConnectionSidebarItemViews.swift b/Echo/Sources/Features/ConnectionVault/Views/Sidebar/ConnectionSidebarItemViews.swift index 511c482df..a491222e0 100644 --- a/Echo/Sources/Features/ConnectionVault/Views/Sidebar/ConnectionSidebarItemViews.swift +++ b/Echo/Sources/Features/ConnectionVault/Views/Sidebar/ConnectionSidebarItemViews.swift @@ -148,12 +148,7 @@ struct ConnectionListRow: View { @ViewBuilder private var connectionIcon: some View { -#if os(macOS) - if let data = connection.logo, let img = NSImage(data: data) { Image(nsImage: img).resizable().aspectRatio(contentMode: .fit).clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous)) } - else { Image(connection.databaseType.iconName).resizable().renderingMode(.template).aspectRatio(contentMode: .fit).foregroundStyle(accentColor) } -#else - Image(connection.databaseType.iconName).resizable().renderingMode(.template).aspectRatio(contentMode: .fit).foregroundStyle(accentColor) -#endif + DatabaseTypeIcon(databaseType: connection.databaseType, tint: accentColor) } @ViewBuilder diff --git a/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel+Actions.swift b/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel+Actions.swift new file mode 100644 index 000000000..bf476c0f1 --- /dev/null +++ b/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel+Actions.swift @@ -0,0 +1,124 @@ +import Foundation +import AppKit +import Logging +import UniformTypeIdentifiers + +// MARK: - Database Loading, Object Selection, and Output Delivery + +extension DataMigrationWizardViewModel { + + // MARK: - Database Loading + + func loadSourceDatabases() { + guard let session = sourceSession else { return } + isLoadingSourceDatabases = true + Task { + do { + let databases = try await session.session.listDatabases() + self.sourceDatabases = databases + if self.sourceDatabaseName.isEmpty, let first = databases.first { + self.sourceDatabaseName = first + } + } catch { + logger.error("Failed to load source databases: \(error)") + } + self.isLoadingSourceDatabases = false + } + } + + func loadTargetDatabases() { + guard let session = targetSession else { return } + isLoadingTargetDatabases = true + Task { + do { + let databases = try await session.session.listDatabases() + self.targetDatabases = databases + if self.targetDatabaseName.isEmpty, let first = databases.first { + self.targetDatabaseName = first + } + } catch { + logger.error("Failed to load target databases: \(error)") + } + self.isLoadingTargetDatabases = false + } + } + + // MARK: - Object Loading + + func loadSourceObjects() { + guard let session = sourceSession else { return } + isLoadingObjects = true + Task { + do { + let dbSession = try await session.session.sessionForDatabase(sourceDatabaseName) + let schemas = try await dbSession.listSchemas() + var allObjects: [MigrationObject] = [] + for schema in schemas { + let tables = try await dbSession.listTablesAndViews(schema: schema) + for obj in tables where obj.type == .table { + allObjects.append(MigrationObject( + id: "\(obj.schema).\(obj.name)", + schema: obj.schema, + name: obj.name, + objectType: "Table" + )) + } + } + self.sourceObjects = allObjects + self.selectedObjectIDs = Set(allObjects.map(\.id)) + } catch { + logger.error("Failed to load source objects: \(error)") + } + self.isLoadingObjects = false + } + } + + // MARK: - Object Selection + + func selectAll() { + selectedObjectIDs = Set(sourceObjects.map(\.id)) + } + + func deselectAll() { + selectedObjectIDs.removeAll() + } + + func toggleObject(_ obj: MigrationObject) { + if selectedObjectIDs.contains(obj.id) { + selectedObjectIDs.remove(obj.id) + } else { + selectedObjectIDs.insert(obj.id) + } + } + + // MARK: - Output Delivery + + func deliverOutput() { + switch outputDestination { + case .execute: + executeMigration() + case .queryTab: + onOpenInQueryTab?(generatedSQL) + case .clipboard: + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(generatedSQL, forType: .string) + case .file: + saveToFile() + } + } + + func saveToFile() { + let panel = NSSavePanel() + panel.allowedContentTypes = [UTType(filenameExtension: "sql")!] + panel.nameFieldStringValue = "migration_\(sourceDatabaseName)_to_\(targetDatabaseName).sql" + panel.canCreateDirectories = true + + if panel.runModal() == .OK, let url = panel.url { + do { + try generatedSQL.write(to: url, atomically: true, encoding: .utf8) + } catch { + logger.error("Failed to save migration script: \(error)") + } + } + } +} diff --git a/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel+SQLGeneration.swift b/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel+SQLGeneration.swift new file mode 100644 index 000000000..91c6031af --- /dev/null +++ b/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel+SQLGeneration.swift @@ -0,0 +1,283 @@ +import Foundation +import Logging + +// MARK: - SQL Generation, Migration Execution, and SQL Helpers + +extension DataMigrationWizardViewModel { + + // MARK: - SQL Generation + + func generateMigrationSQL() { + guard let source = sourceSession, let target = targetSession else { return } + isGenerating = true + let selectedObjects = sourceObjects.filter { selectedObjectIDs.contains($0.id) } + let targetType = target.connection.databaseType + let sourceType = source.connection.databaseType + + Task { + var sql = "-- Data Migration Script\n" + sql += "-- Source: \(source.connection.connectionName) (\(sourceType.displayName)) / \(sourceDatabaseName)\n" + sql += "-- Target: \(target.connection.connectionName) (\(targetType.displayName)) / \(targetDatabaseName)\n" + sql += "-- Generated: \(Date().formatted())\n\n" + + // Fetch real column metadata from source for accurate DDL + do { + let sourceDB = try await source.session.sessionForDatabase(sourceDatabaseName) + + if migrateSchema { + sql += "-- === Schema Migration ===\n\n" + for obj in selectedObjects { + do { + let structure = try await sourceDB.getTableStructureDetails(schema: obj.schema, table: obj.name) + sql += buildCreateTableSQL( + for: obj, + structure: structure, + targetType: targetType + ) + sql += "\n\n" + } catch { + sql += "-- Failed to read structure for \(obj.schema).\(obj.name): \(error.localizedDescription)\n\n" + } + } + } + + if migrateData { + sql += "-- === Data Migration ===\n" + sql += "-- Use 'Execute Migration' to transfer data automatically,\n" + sql += "-- or run the INSERT statements below against the target.\n\n" + for obj in selectedObjects { + sql += "-- Data for \(obj.schema).\(obj.name) will be transferred during execution.\n" + } + } + } catch { + sql += "-- Error accessing source database: \(error.localizedDescription)\n" + } + + self.generatedSQL = sql + self.isGenerating = false + } + } + + // MARK: - Execute Migration + + func executeMigration() { + guard let source = sourceSession, let target = targetSession else { return } + isMigrating = true + migrationProgress = 0 + migrationStatus = "Starting migration..." + migrationLog = [] + migrationError = nil + + let selectedObjects = sourceObjects.filter { selectedObjectIDs.contains($0.id) } + let targetType = target.connection.databaseType + + Task { + do { + let targetDB = try await target.session.sessionForDatabase(targetDatabaseName) + let sourceDB = try await source.session.sessionForDatabase(sourceDatabaseName) + let total = Double(selectedObjects.count) + + for (index, obj) in selectedObjects.enumerated() { + self.migrationProgress = Double(index) / max(total, 1) + self.migrationStatus = "Migrating \(obj.schema).\(obj.name)..." + + if migrateSchema { + do { + let structure = try await sourceDB.getTableStructureDetails(schema: obj.schema, table: obj.name) + let ddl = buildCreateTableSQL(for: obj, structure: structure, targetType: targetType) + + if dropTargetIfExists { + let dropSQL = dropTableSQL(for: obj, targetType: targetType) + _ = try? await targetDB.simpleQuery(dropSQL) + } + _ = try await targetDB.simpleQuery(ddl) + appendLog("Created table \(obj.schema).\(obj.name)") + } catch { + appendLog("Schema failed for \(obj.schema).\(obj.name): \(error.localizedDescription)") + if !continueOnError { throw error } + } + } + + if migrateData { + do { + let sourceQualified = qualifiedName(obj, targetType: source.connection.databaseType) + let result = try await sourceDB.simpleQuery("SELECT * FROM \(sourceQualified)") + let rowCount = result.rows.count + if rowCount > 0 { + let insertBatches = buildInsertStatements( + for: obj, + columns: result.columns, + rows: result.rows, + targetType: targetType + ) + for batch in insertBatches { + _ = try await targetDB.simpleQuery(batch) + } + appendLog("Migrated \(rowCount) rows to \(obj.name)") + } else { + appendLog("No data in \(obj.name)") + } + } catch { + appendLog("Data transfer failed for \(obj.name): \(error.localizedDescription)") + if !continueOnError { throw error } + } + } + } + + self.migrationProgress = 1.0 + self.migrationStatus = "Migration completed successfully" + self.migrationSucceeded = true + } catch { + self.migrationError = error.localizedDescription + self.migrationStatus = "Migration failed" + logger.error("Migration failed: \(error)") + } + self.isMigrating = false + } + } + + // MARK: - SQL Helpers + + func qualifiedName(_ obj: MigrationObject, targetType: DatabaseType) -> String { + switch targetType { + case .microsoftSQL: + return "[\(obj.schema)].[\(obj.name)]" + case .postgresql: + return "\"\(obj.schema)\".\"\(obj.name)\"" + case .mysql: + return "`\(obj.name)`" + case .sqlite: + return "\"\(obj.name)\"" + } + } + + func buildCreateTableSQL( + for obj: MigrationObject, + structure: TableStructureDetails, + targetType: DatabaseType + ) -> String { + let name = qualifiedName(obj, targetType: targetType) + let pkColumns = Set(structure.primaryKey?.columns ?? []) + let columnDefs = structure.columns.map { col in + let colName = quoteName(col.name, targetType: targetType) + let typeName = mapDataType(col.dataType, targetType: targetType) + let nullable = col.isNullable ? "" : " NOT NULL" + let pk = pkColumns.contains(col.name) ? " PRIMARY KEY" : "" + return " \(colName) \(typeName)\(nullable)\(pk)" + } + + let prefix: String + switch targetType { + case .microsoftSQL: + prefix = "CREATE TABLE \(name)" + default: + prefix = "CREATE TABLE IF NOT EXISTS \(name)" + } + + var sql = "\(prefix) (\n\(columnDefs.joined(separator: ",\n"))\n)" + if targetType == .microsoftSQL { + sql += ";\nGO" + } else { + sql += ";" + } + return sql + } + + func dropTableSQL(for obj: MigrationObject, targetType: DatabaseType) -> String { + let name = qualifiedName(obj, targetType: targetType) + switch targetType { + case .microsoftSQL: + return "DROP TABLE IF EXISTS \(name);\nGO" + default: + return "DROP TABLE IF EXISTS \(name);" + } + } + + func quoteName(_ name: String, targetType: DatabaseType) -> String { + switch targetType { + case .microsoftSQL: return "[\(name)]" + case .mysql: return "`\(name)`" + default: return "\"\(name)\"" + } + } + + func mapDataType(_ sourceType: String, targetType: DatabaseType) -> String { + let normalized = sourceType.uppercased() + + switch targetType { + case .mysql: + if normalized.hasPrefix("NVARCHAR") || normalized.hasPrefix("CHARACTER VARYING") { return "VARCHAR(255)" } + if normalized == "TEXT" || normalized == "NTEXT" { return "TEXT" } + if normalized == "SERIAL" || normalized == "BIGSERIAL" { return "BIGINT AUTO_INCREMENT" } + if normalized == "BOOLEAN" || normalized == "BOOL" { return "TINYINT(1)" } + if normalized.hasPrefix("TIMESTAMP") { return "DATETIME" } + if normalized == "BYTEA" || normalized == "VARBINARY(MAX)" { return "LONGBLOB" } + if normalized == "UUID" || normalized == "UNIQUEIDENTIFIER" { return "CHAR(36)" } + return sourceType + case .postgresql: + if normalized.hasPrefix("NVARCHAR") || normalized.hasPrefix("VARCHAR") { return "TEXT" } + if normalized == "INT" || normalized == "INTEGER" { return "INTEGER" } + if normalized == "BIGINT" { return "BIGINT" } + if normalized == "BIT" || normalized == "TINYINT(1)" { return "BOOLEAN" } + if normalized == "DATETIME" || normalized == "DATETIME2" { return "TIMESTAMP" } + if normalized == "VARBINARY(MAX)" || normalized == "LONGBLOB" { return "BYTEA" } + if normalized == "UNIQUEIDENTIFIER" || normalized == "CHAR(36)" { return "UUID" } + return sourceType + case .microsoftSQL: + if normalized == "TEXT" { return "NVARCHAR(MAX)" } + if normalized == "SERIAL" { return "INT IDENTITY(1,1)" } + if normalized == "BIGSERIAL" { return "BIGINT IDENTITY(1,1)" } + if normalized == "BOOLEAN" || normalized == "BOOL" { return "BIT" } + if normalized == "TIMESTAMP" || normalized.hasPrefix("TIMESTAMP") { return "DATETIME2" } + if normalized == "BYTEA" || normalized == "LONGBLOB" { return "VARBINARY(MAX)" } + if normalized == "UUID" { return "UNIQUEIDENTIFIER" } + return sourceType + case .sqlite: + if normalized.contains("INT") { return "INTEGER" } + if normalized.contains("CHAR") || normalized.contains("TEXT") || normalized.contains("CLOB") { return "TEXT" } + if normalized.contains("BLOB") || normalized == "BYTEA" { return "BLOB" } + if normalized.contains("REAL") || normalized.contains("FLOAT") || normalized.contains("DOUBLE") { return "REAL" } + return sourceType + } + } + + func buildInsertStatements( + for obj: MigrationObject, + columns: [ColumnInfo], + rows: [[String?]], + targetType: DatabaseType + ) -> [String] { + guard !rows.isEmpty, !columns.isEmpty else { return [] } + + let tableName = qualifiedName(obj, targetType: targetType) + let colNames = columns.map { quoteName($0.name, targetType: targetType) }.joined(separator: ", ") + + var batches: [String] = [] + + for chunk in stride(from: 0, to: rows.count, by: batchSize) { + let end = min(chunk + batchSize, rows.count) + let batchRows = rows[chunk.. String { + guard let value, value != "(null)" else { return "NULL" } + let escaped = value.replacingOccurrences(of: "'", with: "''") + return "'\(escaped)'" + } + + func appendLog(_ message: String) { + migrationLog.append("[\(Date().formatted(date: .omitted, time: .standard))] \(message)") + } +} diff --git a/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel.swift b/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel.swift index 03b8d34fc..fd567ab06 100644 --- a/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel.swift +++ b/Echo/Sources/Features/DataMigration/Domain/DataMigrationWizardViewModel.swift @@ -83,7 +83,7 @@ final class DataMigrationWizardViewModel { // MARK: - Dependencies var onOpenInQueryTab: ((String) -> Void)? - private let logger = Logger(label: "DataMigrationWizardViewModel") + let logger = Logger(label: "DataMigrationWizardViewModel") // MARK: - Navigation @@ -128,395 +128,4 @@ final class DataMigrationWizardViewModel { guard let id = targetSessionID else { return nil } return availableSessions.first(where: { $0.id == id }) } - - // MARK: - Database Loading - - func loadSourceDatabases() { - guard let session = sourceSession else { return } - isLoadingSourceDatabases = true - Task { - do { - let databases = try await session.session.listDatabases() - self.sourceDatabases = databases - if self.sourceDatabaseName.isEmpty, let first = databases.first { - self.sourceDatabaseName = first - } - } catch { - logger.error("Failed to load source databases: \(error)") - } - self.isLoadingSourceDatabases = false - } - } - - func loadTargetDatabases() { - guard let session = targetSession else { return } - isLoadingTargetDatabases = true - Task { - do { - let databases = try await session.session.listDatabases() - self.targetDatabases = databases - if self.targetDatabaseName.isEmpty, let first = databases.first { - self.targetDatabaseName = first - } - } catch { - logger.error("Failed to load target databases: \(error)") - } - self.isLoadingTargetDatabases = false - } - } - - // MARK: - Object Loading - - func loadSourceObjects() { - guard let session = sourceSession else { return } - isLoadingObjects = true - Task { - do { - let dbSession = try await session.session.sessionForDatabase(sourceDatabaseName) - let schemas = try await dbSession.listSchemas() - var allObjects: [MigrationObject] = [] - for schema in schemas { - let tables = try await dbSession.listTablesAndViews(schema: schema) - for obj in tables where obj.type == .table { - allObjects.append(MigrationObject( - id: "\(obj.schema).\(obj.name)", - schema: obj.schema, - name: obj.name, - objectType: "Table" - )) - } - } - self.sourceObjects = allObjects - self.selectedObjectIDs = Set(allObjects.map(\.id)) - } catch { - logger.error("Failed to load source objects: \(error)") - } - self.isLoadingObjects = false - } - } - - // MARK: - Object Selection - - func selectAll() { - selectedObjectIDs = Set(sourceObjects.map(\.id)) - } - - func deselectAll() { - selectedObjectIDs.removeAll() - } - - func toggleObject(_ obj: MigrationObject) { - if selectedObjectIDs.contains(obj.id) { - selectedObjectIDs.remove(obj.id) - } else { - selectedObjectIDs.insert(obj.id) - } - } - - // MARK: - SQL Generation - - func generateMigrationSQL() { - guard let source = sourceSession, let target = targetSession else { return } - isGenerating = true - let selectedObjects = sourceObjects.filter { selectedObjectIDs.contains($0.id) } - let targetType = target.connection.databaseType - let sourceType = source.connection.databaseType - - Task { - var sql = "-- Data Migration Script\n" - sql += "-- Source: \(source.connection.connectionName) (\(sourceType.displayName)) / \(sourceDatabaseName)\n" - sql += "-- Target: \(target.connection.connectionName) (\(targetType.displayName)) / \(targetDatabaseName)\n" - sql += "-- Generated: \(Date().formatted())\n\n" - - // Fetch real column metadata from source for accurate DDL - do { - let sourceDB = try await source.session.sessionForDatabase(sourceDatabaseName) - - if migrateSchema { - sql += "-- === Schema Migration ===\n\n" - for obj in selectedObjects { - do { - let structure = try await sourceDB.getTableStructureDetails(schema: obj.schema, table: obj.name) - sql += buildCreateTableSQL( - for: obj, - structure: structure, - targetType: targetType - ) - sql += "\n\n" - } catch { - sql += "-- Failed to read structure for \(obj.schema).\(obj.name): \(error.localizedDescription)\n\n" - } - } - } - - if migrateData { - sql += "-- === Data Migration ===\n" - sql += "-- Use 'Execute Migration' to transfer data automatically,\n" - sql += "-- or run the INSERT statements below against the target.\n\n" - for obj in selectedObjects { - sql += "-- Data for \(obj.schema).\(obj.name) will be transferred during execution.\n" - } - } - } catch { - sql += "-- Error accessing source database: \(error.localizedDescription)\n" - } - - self.generatedSQL = sql - self.isGenerating = false - } - } - - // MARK: - Execute Migration - - func executeMigration() { - guard let source = sourceSession, let target = targetSession else { return } - isMigrating = true - migrationProgress = 0 - migrationStatus = "Starting migration..." - migrationLog = [] - migrationError = nil - - let selectedObjects = sourceObjects.filter { selectedObjectIDs.contains($0.id) } - let targetType = target.connection.databaseType - - Task { - do { - let targetDB = try await target.session.sessionForDatabase(targetDatabaseName) - let sourceDB = try await source.session.sessionForDatabase(sourceDatabaseName) - let total = Double(selectedObjects.count) - - for (index, obj) in selectedObjects.enumerated() { - self.migrationProgress = Double(index) / max(total, 1) - self.migrationStatus = "Migrating \(obj.schema).\(obj.name)..." - - if migrateSchema { - do { - let structure = try await sourceDB.getTableStructureDetails(schema: obj.schema, table: obj.name) - let ddl = buildCreateTableSQL(for: obj, structure: structure, targetType: targetType) - - if dropTargetIfExists { - let dropSQL = dropTableSQL(for: obj, targetType: targetType) - _ = try? await targetDB.simpleQuery(dropSQL) - } - _ = try await targetDB.simpleQuery(ddl) - appendLog("Created table \(obj.schema).\(obj.name)") - } catch { - appendLog("Schema failed for \(obj.schema).\(obj.name): \(error.localizedDescription)") - if !continueOnError { throw error } - } - } - - if migrateData { - do { - let sourceQualified = qualifiedName(obj, targetType: source.connection.databaseType) - let result = try await sourceDB.simpleQuery("SELECT * FROM \(sourceQualified)") - let rowCount = result.rows.count - if rowCount > 0 { - let insertBatches = buildInsertStatements( - for: obj, - columns: result.columns, - rows: result.rows, - targetType: targetType - ) - for batch in insertBatches { - _ = try await targetDB.simpleQuery(batch) - } - appendLog("Migrated \(rowCount) rows to \(obj.name)") - } else { - appendLog("No data in \(obj.name)") - } - } catch { - appendLog("Data transfer failed for \(obj.name): \(error.localizedDescription)") - if !continueOnError { throw error } - } - } - } - - self.migrationProgress = 1.0 - self.migrationStatus = "Migration completed successfully" - self.migrationSucceeded = true - } catch { - self.migrationError = error.localizedDescription - self.migrationStatus = "Migration failed" - logger.error("Migration failed: \(error)") - } - self.isMigrating = false - } - } - - // MARK: - Output Delivery - - func deliverOutput() { - switch outputDestination { - case .execute: - executeMigration() - case .queryTab: - onOpenInQueryTab?(generatedSQL) - case .clipboard: - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(generatedSQL, forType: .string) - case .file: - saveToFile() - } - } - - func saveToFile() { - let panel = NSSavePanel() - panel.allowedContentTypes = [UTType(filenameExtension: "sql")!] - panel.nameFieldStringValue = "migration_\(sourceDatabaseName)_to_\(targetDatabaseName).sql" - panel.canCreateDirectories = true - - if panel.runModal() == .OK, let url = panel.url { - do { - try generatedSQL.write(to: url, atomically: true, encoding: .utf8) - } catch { - logger.error("Failed to save migration script: \(error)") - } - } - } - - // MARK: - SQL Helpers - - private func qualifiedName(_ obj: MigrationObject, targetType: DatabaseType) -> String { - switch targetType { - case .microsoftSQL: - return "[\(obj.schema)].[\(obj.name)]" - case .postgresql: - return "\"\(obj.schema)\".\"\(obj.name)\"" - case .mysql: - return "`\(obj.name)`" - case .sqlite: - return "\"\(obj.name)\"" - } - } - - private func buildCreateTableSQL( - for obj: MigrationObject, - structure: TableStructureDetails, - targetType: DatabaseType - ) -> String { - let name = qualifiedName(obj, targetType: targetType) - let pkColumns = Set(structure.primaryKey?.columns ?? []) - let columnDefs = structure.columns.map { col in - let colName = quoteName(col.name, targetType: targetType) - let typeName = mapDataType(col.dataType, targetType: targetType) - let nullable = col.isNullable ? "" : " NOT NULL" - let pk = pkColumns.contains(col.name) ? " PRIMARY KEY" : "" - return " \(colName) \(typeName)\(nullable)\(pk)" - } - - let prefix: String - switch targetType { - case .microsoftSQL: - prefix = "CREATE TABLE \(name)" - default: - prefix = "CREATE TABLE IF NOT EXISTS \(name)" - } - - var sql = "\(prefix) (\n\(columnDefs.joined(separator: ",\n"))\n)" - if targetType == .microsoftSQL { - sql += ";\nGO" - } else { - sql += ";" - } - return sql - } - - private func dropTableSQL(for obj: MigrationObject, targetType: DatabaseType) -> String { - let name = qualifiedName(obj, targetType: targetType) - switch targetType { - case .microsoftSQL: - return "DROP TABLE IF EXISTS \(name);\nGO" - default: - return "DROP TABLE IF EXISTS \(name);" - } - } - - private func quoteName(_ name: String, targetType: DatabaseType) -> String { - switch targetType { - case .microsoftSQL: return "[\(name)]" - case .mysql: return "`\(name)`" - default: return "\"\(name)\"" - } - } - - private func mapDataType(_ sourceType: String, targetType: DatabaseType) -> String { - let normalized = sourceType.uppercased() - - switch targetType { - case .mysql: - if normalized.hasPrefix("NVARCHAR") || normalized.hasPrefix("CHARACTER VARYING") { return "VARCHAR(255)" } - if normalized == "TEXT" || normalized == "NTEXT" { return "TEXT" } - if normalized == "SERIAL" || normalized == "BIGSERIAL" { return "BIGINT AUTO_INCREMENT" } - if normalized == "BOOLEAN" || normalized == "BOOL" { return "TINYINT(1)" } - if normalized.hasPrefix("TIMESTAMP") { return "DATETIME" } - if normalized == "BYTEA" || normalized == "VARBINARY(MAX)" { return "LONGBLOB" } - if normalized == "UUID" || normalized == "UNIQUEIDENTIFIER" { return "CHAR(36)" } - return sourceType - case .postgresql: - if normalized.hasPrefix("NVARCHAR") || normalized.hasPrefix("VARCHAR") { return "TEXT" } - if normalized == "INT" || normalized == "INTEGER" { return "INTEGER" } - if normalized == "BIGINT" { return "BIGINT" } - if normalized == "BIT" || normalized == "TINYINT(1)" { return "BOOLEAN" } - if normalized == "DATETIME" || normalized == "DATETIME2" { return "TIMESTAMP" } - if normalized == "VARBINARY(MAX)" || normalized == "LONGBLOB" { return "BYTEA" } - if normalized == "UNIQUEIDENTIFIER" || normalized == "CHAR(36)" { return "UUID" } - return sourceType - case .microsoftSQL: - if normalized == "TEXT" { return "NVARCHAR(MAX)" } - if normalized == "SERIAL" { return "INT IDENTITY(1,1)" } - if normalized == "BIGSERIAL" { return "BIGINT IDENTITY(1,1)" } - if normalized == "BOOLEAN" || normalized == "BOOL" { return "BIT" } - if normalized == "TIMESTAMP" || normalized.hasPrefix("TIMESTAMP") { return "DATETIME2" } - if normalized == "BYTEA" || normalized == "LONGBLOB" { return "VARBINARY(MAX)" } - if normalized == "UUID" { return "UNIQUEIDENTIFIER" } - return sourceType - case .sqlite: - if normalized.contains("INT") { return "INTEGER" } - if normalized.contains("CHAR") || normalized.contains("TEXT") || normalized.contains("CLOB") { return "TEXT" } - if normalized.contains("BLOB") || normalized == "BYTEA" { return "BLOB" } - if normalized.contains("REAL") || normalized.contains("FLOAT") || normalized.contains("DOUBLE") { return "REAL" } - return sourceType - } - } - - private func buildInsertStatements( - for obj: MigrationObject, - columns: [ColumnInfo], - rows: [[String?]], - targetType: DatabaseType - ) -> [String] { - guard !rows.isEmpty, !columns.isEmpty else { return [] } - - let tableName = qualifiedName(obj, targetType: targetType) - let colNames = columns.map { quoteName($0.name, targetType: targetType) }.joined(separator: ", ") - - var batches: [String] = [] - - for chunk in stride(from: 0, to: rows.count, by: batchSize) { - let end = min(chunk + batchSize, rows.count) - let batchRows = rows[chunk.. String { - guard let value, value != "(null)" else { return "NULL" } - let escaped = value.replacingOccurrences(of: "'", with: "''") - return "'\(escaped)'" - } - - private func appendLog(_ message: String) { - migrationLog.append("[\(Date().formatted(date: .omitted, time: .standard))] \(message)") - } } diff --git a/Echo/Sources/Features/GenerateScripts/Views/DACWizardView.swift b/Echo/Sources/Features/GenerateScripts/Views/DACWizardView.swift index c2bce8b56..32c203ed4 100644 --- a/Echo/Sources/Features/GenerateScripts/Views/DACWizardView.swift +++ b/Echo/Sources/Features/GenerateScripts/Views/DACWizardView.swift @@ -125,7 +125,7 @@ struct DACWizardView: View { if let error = viewModel.errorMessage { Image(systemName: "xmark.circle.fill") .font(TypographyTokens.iconHero) - .foregroundStyle(.red) + .foregroundStyle(ColorTokens.Status.error) Text("Operation Failed") .font(TypographyTokens.title) Text(error) @@ -134,7 +134,7 @@ struct DACWizardView: View { } else { Image(systemName: "checkmark.circle.fill") .font(TypographyTokens.iconHero) - .foregroundStyle(.green) + .foregroundStyle(ColorTokens.Status.success) Text("Success") .font(TypographyTokens.title) Text("The operation completed successfully.") diff --git a/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheModels.swift b/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheModels.swift new file mode 100644 index 000000000..deabe92c6 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheModels.swift @@ -0,0 +1,28 @@ +import Foundation + +struct ObjectBrowserCacheKey: Hashable, Codable, Sendable { + let connectionID: UUID +} + +struct ObjectBrowserCacheEntry: Codable, Sendable { + static let currentSchemaVersion = 1 + + let schemaVersion: Int + let key: ObjectBrowserCacheKey + let connectionFingerprint: String + let updatedAt: Date + let structure: DatabaseStructure + + init( + key: ObjectBrowserCacheKey, + connectionFingerprint: String, + updatedAt: Date = Date(), + structure: DatabaseStructure + ) { + self.schemaVersion = Self.currentSchemaVersion + self.key = key + self.connectionFingerprint = connectionFingerprint + self.updatedAt = updatedAt + self.structure = structure + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift b/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift new file mode 100644 index 000000000..2161da096 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift @@ -0,0 +1,137 @@ +import Foundation + +actor ObjectBrowserCacheStore { + struct Configuration: Sendable { + let rootDirectory: URL + } + + private let configuration: Configuration + private let fileManager = FileManager.default + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + init(configuration: Configuration) { + self.configuration = configuration + if !fileManager.fileExists(atPath: configuration.rootDirectory.path) { + try? fileManager.createDirectory( + at: configuration.rootDirectory, + withIntermediateDirectories: true + ) + } + } + + static func defaultRootDirectory() -> URL { + let base = (try? FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + )) ?? FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent("Library/Application Support", isDirectory: true) + return base + .appendingPathComponent("Echo", isDirectory: true) + .appendingPathComponent("ObjectBrowserCache", isDirectory: true) + } + + func entry(for connection: SavedConnection) async -> ObjectBrowserCacheEntry? { + let url = cacheURL(for: ObjectBrowserCacheKey(connectionID: connection.id)) + guard fileManager.fileExists(atPath: url.path) else { return nil } + guard let data = try? Data(contentsOf: url), + let entry = try? decoder.decode(ObjectBrowserCacheEntry.self, from: data), + entry.schemaVersion == ObjectBrowserCacheEntry.currentSchemaVersion, + entry.connectionFingerprint == connection.objectBrowserCacheFingerprint else { + return nil + } + return entry + } + + func migrateLegacyCacheIfNeeded( + from connection: SavedConnection, + limitBytes: Int + ) async { + guard let legacyStructure = connection.cachedStructure else { return } + if await entry(for: connection) != nil { + return + } + let entry = ObjectBrowserCacheEntry( + key: ObjectBrowserCacheKey(connectionID: connection.id), + connectionFingerprint: connection.objectBrowserCacheFingerprint, + updatedAt: connection.cachedStructureUpdatedAt ?? Date(), + structure: legacyStructure + ) + try? await write(entry, limitBytes: limitBytes) + } + + func stashStructure( + _ structure: DatabaseStructure, + for connection: SavedConnection, + limitBytes: Int + ) async throws { + let entry = ObjectBrowserCacheEntry( + key: ObjectBrowserCacheKey(connectionID: connection.id), + connectionFingerprint: connection.objectBrowserCacheFingerprint, + structure: structure + ) + try await write(entry, limitBytes: limitBytes) + } + + func currentUsageBytes() async -> UInt64 { + let urls = cacheFileURLs() + return urls.reduce(into: UInt64(0)) { total, url in + let values = try? url.resourceValues(forKeys: [.fileSizeKey]) + total += UInt64(values?.fileSize ?? 0) + } + } + + func removeAll() async { + for url in cacheFileURLs() { + try? fileManager.removeItem(at: url) + } + } + + func pruneToLimit(_ limitBytes: Int) async { + let normalizedLimit = max(limitBytes, 64 * 1_024 * 1_024) + var entries: [(url: URL, updatedAt: Date, size: Int)] = cacheFileURLs().compactMap { url in + guard let data = try? Data(contentsOf: url), + let entry = try? decoder.decode(ObjectBrowserCacheEntry.self, from: data) else { + return nil + } + return (url, entry.updatedAt, data.count) + } + var total = entries.reduce(0) { $0 + $1.size } + guard total > normalizedLimit else { return } + + entries.sort { $0.updatedAt < $1.updatedAt } + for entry in entries where total > normalizedLimit { + try? fileManager.removeItem(at: entry.url) + total -= entry.size + } + } + + private func write(_ entry: ObjectBrowserCacheEntry, limitBytes: Int) async throws { + if !fileManager.fileExists(atPath: configuration.rootDirectory.path) { + try fileManager.createDirectory( + at: configuration.rootDirectory, + withIntermediateDirectories: true + ) + } + let data = try encoder.encode(entry) + try data.write(to: cacheURL(for: entry.key), options: [.atomic]) + await pruneToLimit(limitBytes) + } + + private func cacheURL(for key: ObjectBrowserCacheKey) -> URL { + configuration.rootDirectory + .appendingPathComponent(key.connectionID.uuidString) + .appendingPathExtension("json") + } + + private func cacheFileURLs() -> [URL] { + let urls = (try? fileManager.contentsOfDirectory( + at: configuration.rootDirectory, + includingPropertiesForKeys: [.contentModificationDateKey, .fileSizeKey], + options: [.skipsHiddenFiles] + )) ?? [] + return urls.filter { $0.pathExtension == "json" } + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Discovery/SchemaDiscoveryEngine.swift b/Echo/Sources/Features/ObjectBrowser/Discovery/SchemaDiscoveryEngine.swift index c010933bd..bc659b204 100644 --- a/Echo/Sources/Features/ObjectBrowser/Discovery/SchemaDiscoveryEngine.swift +++ b/Echo/Sources/Features/ObjectBrowser/Discovery/SchemaDiscoveryEngine.swift @@ -5,13 +5,20 @@ import Observation final class MetadataDiscoveryEngine: MetadataDiscoveryEngineProtocol, @unchecked Sendable { private let identityRepository: IdentityRepository private let connectionStore: ConnectionStore + private let objectBrowserCacheStore: ObjectBrowserCacheStore var onPersistConnections: (@MainActor @Sendable () async -> Void)? var onEnqueuePrefetch: (@MainActor @Sendable (ConnectionSession) async -> Void)? + var cacheLimitProvider: (@MainActor @Sendable () -> Int)? - init(identityRepository: IdentityRepository, connectionStore: ConnectionStore) { + init( + identityRepository: IdentityRepository, + connectionStore: ConnectionStore, + objectBrowserCacheStore: ObjectBrowserCacheStore + ) { self.identityRepository = identityRepository self.connectionStore = connectionStore + self.objectBrowserCacheStore = objectBrowserCacheStore } // MARK: - Core Discovery @@ -54,6 +61,7 @@ final class MetadataDiscoveryEngine: MetadataDiscoveryEngineProtocol, @unchecked if connectionSession.databaseStructure == nil { connectionSession.databaseStructure = DatabaseStructure(serverVersion: nil, databases: []) + connectionSession.reconcileMetadataFreshnessFromLiveStructure() } guard let credentials = identityRepository.resolveAuthenticationConfiguration(for: connectionSession.connection, overridePassword: nil) else { @@ -135,6 +143,12 @@ final class MetadataDiscoveryEngine: MetadataDiscoveryEngineProtocol, @unchecked let finalStructure = DatabaseStructure(serverVersion: interimServerVersion, databases: mergedDatabases) print("[PERF] initialLoad: applying final structure with \(mergedDatabases.count) databases (single UI update)") self.applyStructureUpdate(finalStructure, to: connectionSession, cacheResult: true) + let liveDatabases = Set(structure.databases.compactMap { database in + database.schemas.contains(where: { !$0.objects.isEmpty }) + ? connectionSession.schemaLoadKey(database.name) + : nil + }) + connectionSession.reconcileMetadataFreshnessFromLiveStructure(markingLive: liveDatabases) print("[PERF] initialLoad: done") connectionSession.structureLoadingState = .ready @@ -199,11 +213,14 @@ final class MetadataDiscoveryEngine: MetadataDiscoveryEngineProtocol, @unchecked for db in structure.databases { let mergeStart = CFAbsoluteTimeGetCurrent() mergeSingleDatabase(db, into: session) + let hasSchemas = db.schemas.contains(where: { !$0.objects.isEmpty }) + session.markMetadataRefreshCompleted(forDatabase: db.name, hasSchemas: hasSchemas) print("[PERF] \(databaseName): final merge took \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - mergeStart))s") } print("[PERF] \(databaseName): total loadDatabaseSchemaOnly \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - t0))s") } catch { + session.markMetadataRefreshFailed(forDatabase: databaseName) ConnectionDebug.log("[SchemaDiscovery] loadDatabaseSchemaOnly failed for '\(databaseName)': \(error.localizedDescription)") } } @@ -266,9 +283,15 @@ final class MetadataDiscoveryEngine: MetadataDiscoveryEngineProtocol, @unchecked private func schedulePersist(_ structure: DatabaseStructure, for session: ConnectionSession) { persistTask?.cancel() + let connection = session.connection + let limitBytes = cacheLimitProvider?() ?? 512 * 1_024 * 1_024 persistTask = Task { - try? await Task.sleep(for: .milliseconds(500)) guard !Task.isCancelled else { return } + try? await self.objectBrowserCacheStore.stashStructure( + structure, + for: connection, + limitBytes: limitBytes + ) var conn = session.connection conn.cachedStructure = structure conn.cachedStructureUpdatedAt = Date() diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserNode.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserNode.swift new file mode 100644 index 000000000..1114a5e47 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserNode.swift @@ -0,0 +1,178 @@ +import Foundation +import SQLServerKit + +enum ExperimentalObjectBrowserServerFolderKind: String { + case security + case databaseSnapshots + case agentJobs + case management + case ssis + case linkedServers + case serverTriggers + + var title: String { + switch self { + case .security: "Security" + case .databaseSnapshots: "Database Snapshots" + case .agentJobs: "Agent Jobs" + case .management: "Management" + case .ssis: "Integration Services Catalogs" + case .linkedServers: "Linked Servers" + case .serverTriggers: "Server Triggers" + } + } + + var systemImage: String { + switch self { + case .security: "shield" + case .databaseSnapshots: "camera.aperture" + case .agentJobs: "clock" + case .management: "gearshape" + case .ssis: "shippingbox" + case .linkedServers: "link" + case .serverTriggers: "bolt.badge.clock" + } + } +} + +enum ExperimentalObjectBrowserSecuritySectionKind: String { + case logins + case certificateLogins + case serverRoles + case credentials + case pgLoginRoles + case pgGroupRoles + + var title: String { + switch self { + case .logins: "Logins" + case .certificateLogins: "Certificate Logins" + case .serverRoles: "Server Roles" + case .credentials: "Credentials" + case .pgLoginRoles: "Login Roles" + case .pgGroupRoles: "Group Roles" + } + } + + var systemImage: String { + switch self { + case .logins: "person.2" + case .certificateLogins: "doc.badge.lock" + case .serverRoles: "shield" + case .credentials: "key" + case .pgLoginRoles: "person.crop.circle" + case .pgGroupRoles: "person.2.circle" + } + } +} + +enum ExperimentalObjectBrowserActionKind: String { + case maintenance + case serverProperties + case activityMonitor + case extendedEvents + case databaseMail + case sqlProfiler + case resourceGovernor + case tuningAdvisor + case policyManagement + case sqlServerLogs + case openJobQueue + + var title: String { + switch self { + case .maintenance: "Maintenance" + case .serverProperties: "Server Properties" + case .activityMonitor: "Activity Monitor" + case .extendedEvents: "Extended Events" + case .databaseMail: "Database Mail" + case .sqlProfiler: "SQL Profiler" + case .resourceGovernor: "Resource Governor" + case .tuningAdvisor: "Tuning Advisor" + case .policyManagement: "Policy Management" + case .sqlServerLogs: "SQL Server Logs" + case .openJobQueue: "Agent Jobs Overview" + } + } + + var systemImage: String { + switch self { + case .maintenance: "wrench.and.screwdriver" + case .serverProperties: "gearshape.2" + case .activityMonitor: "gauge.high" + case .extendedEvents: "list.bullet.rectangle" + case .databaseMail: "envelope" + case .sqlProfiler: "chart.line.uptrend.xyaxis" + case .resourceGovernor: "slider.horizontal.3" + case .tuningAdvisor: "wand.and.stars" + case .policyManagement: "checkmark.shield" + case .sqlServerLogs: "doc.text" + case .openJobQueue: "list.bullet.rectangle" + } + } +} + +enum ExperimentalObjectBrowserDatabaseFolderKind: String { + case security + case databaseTriggers + case serviceBroker + case externalResources + + var title: String { + switch self { + case .security: "Security" + case .databaseTriggers: "Database Triggers" + case .serviceBroker: "Service Broker" + case .externalResources: "External Resources" + } + } + + var systemImage: String { + switch self { + case .security: "shield" + case .databaseTriggers: "bolt" + case .serviceBroker: "tray.2" + case .externalResources: "externaldrive" + } + } +} + +@MainActor +final class ExperimentalObjectBrowserNode: NSObject { + enum Row { + case topSpacer(CGFloat) + case pendingConnection(PendingConnection) + case server(ConnectionSession) + case databasesFolder(ConnectionSession, count: Int) + case database(ConnectionSession, DatabaseInfo, isLoading: Bool) + case objectGroup(ConnectionSession, String, SchemaObjectInfo.ObjectType, count: Int) + case object(ConnectionSession, String, SchemaObjectInfo) + case serverFolder(ConnectionSession, ExperimentalObjectBrowserServerFolderKind, count: Int?) + case databaseFolder(ConnectionSession, String, ExperimentalObjectBrowserDatabaseFolderKind, count: Int?, isLoading: Bool) + case databaseSubfolder(ConnectionSession, String, title: String, systemImage: String, paletteTitle: String, count: Int?) + case databaseNamedItem(ConnectionSession, String, title: String, systemImage: String, paletteTitle: String, detail: String?) + case securitySection(ConnectionSession, ExperimentalObjectBrowserSecuritySectionKind, count: Int, isLoading: Bool) + case securityLogin(ConnectionSession, ExperimentalObjectBrowserSidebarViewModel.SecurityLoginItem) + case securityServerRole(ConnectionSession, ExperimentalObjectBrowserSidebarViewModel.SecurityServerRoleItem) + case securityCredential(ConnectionSession, ExperimentalObjectBrowserSidebarViewModel.SecurityCredentialItem) + case agentJob(ConnectionSession, ExperimentalObjectBrowserSidebarViewModel.AgentJobItem) + case databaseSnapshot(ConnectionSession, SQLServerDatabaseSnapshot) + case linkedServer(ConnectionSession, ExperimentalObjectBrowserSidebarViewModel.LinkedServerItem) + case ssisFolder(ConnectionSession, SQLServerSSISFolder) + case serverTrigger(ConnectionSession, ExperimentalObjectBrowserSidebarViewModel.ServerTriggerItem) + case action(ConnectionSession, ExperimentalObjectBrowserActionKind, depth: Int) + case infoLeaf(String, systemImage: String, paletteTitle: String, depth: Int) + case loading(String, depth: Int) + case message(String, systemImage: String, depth: Int) + } + + let id: String + var row: Row + var children: [ExperimentalObjectBrowserNode] + + init(id: String, row: Row, children: [ExperimentalObjectBrowserNode] = []) { + self.id = id + self.row = row + self.children = children + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserOutlineView.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserOutlineView.swift new file mode 100644 index 000000000..8921750a4 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserOutlineView.swift @@ -0,0 +1,387 @@ +import AppKit +import SwiftUI + +struct ExperimentalObjectBrowserOutlineView: NSViewRepresentable { + let roots: [ExperimentalObjectBrowserNode] + let expandedNodeIDs: Set + let selectedNodeID: String? + let rowContent: (ExperimentalObjectBrowserNode, Bool, Int, CGFloat, @escaping () -> Void) -> AnyView + let onExpansionChanged: (ExperimentalObjectBrowserNode, Bool) -> Void + let onActivation: (ExperimentalObjectBrowserNode) -> Void + let onSelectionChanged: (ExperimentalObjectBrowserNode?) -> Void + let revealNodeID: String? + let revealRequestID: Int + + func makeCoordinator() -> Coordinator { + Coordinator( + rowContent: rowContent, + onExpansionChanged: onExpansionChanged, + onActivation: onActivation, + onSelectionChanged: onSelectionChanged + ) + } + + func makeNSView(context: Context) -> NSScrollView { + let scrollView = NSScrollView() + scrollView.drawsBackground = false + scrollView.borderType = .noBorder + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = false + scrollView.autohidesScrollers = true + scrollView.scrollerStyle = .overlay + scrollView.contentInsets = NSEdgeInsets( + top: 0, + left: 0, + bottom: ExplorerSidebarConstants.scrollBottomPadding + SpacingTokens.md2, + right: 0 + ) + + scrollView.documentView = context.coordinator.tableView + return scrollView + } + + func updateNSView(_ nsView: NSScrollView, context: Context) { + context.coordinator.rowContent = rowContent + context.coordinator.onExpansionChanged = onExpansionChanged + context.coordinator.onActivation = onActivation + context.coordinator.onSelectionChanged = onSelectionChanged + context.coordinator.update( + roots: roots, + expandedNodeIDs: expandedNodeIDs, + selectedNodeID: selectedNodeID, + revealNodeID: revealNodeID, + revealRequestID: revealRequestID + ) + } + + @MainActor + final class Coordinator: NSObject, NSTableViewDataSource, NSTableViewDelegate { + struct VisibleRow { + let node: ExperimentalObjectBrowserNode + let depth: Int + } + + let tableView: NSTableView + var rowContent: (ExperimentalObjectBrowserNode, Bool, Int, CGFloat, @escaping () -> Void) -> AnyView + var onExpansionChanged: (ExperimentalObjectBrowserNode, Bool) -> Void + var onActivation: (ExperimentalObjectBrowserNode) -> Void + var onSelectionChanged: (ExperimentalObjectBrowserNode?) -> Void + + private var roots: [ExperimentalObjectBrowserNode] = [] + private var expandedNodeIDs: Set = [] + private var selectedNodeID: String? + private var visibleRows: [VisibleRow] = [] + private var lastVisibleSignature: [String] = [] + private var lastRevealRequestID = 0 + + init( + rowContent: @escaping (ExperimentalObjectBrowserNode, Bool, Int, CGFloat, @escaping () -> Void) -> AnyView, + onExpansionChanged: @escaping (ExperimentalObjectBrowserNode, Bool) -> Void, + onActivation: @escaping (ExperimentalObjectBrowserNode) -> Void, + onSelectionChanged: @escaping (ExperimentalObjectBrowserNode?) -> Void + ) { + self.rowContent = rowContent + self.onExpansionChanged = onExpansionChanged + self.onActivation = onActivation + self.onSelectionChanged = onSelectionChanged + + let tableView = NSTableView(frame: .zero) + let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("explorer-lab")) + column.resizingMask = .autoresizingMask + tableView.addTableColumn(column) + tableView.headerView = nil + tableView.rowSizeStyle = .default + tableView.selectionHighlightStyle = .none + tableView.focusRingType = .none + tableView.backgroundColor = .clear + tableView.enclosingScrollView?.drawsBackground = false + tableView.intercellSpacing = .zero + tableView.usesAutomaticRowHeights = false + tableView.rowHeight = 25 + tableView.allowsEmptySelection = true + tableView.allowsMultipleSelection = false + + self.tableView = tableView + super.init() + tableView.delegate = self + tableView.dataSource = self + } + + func update( + roots: [ExperimentalObjectBrowserNode], + expandedNodeIDs: Set, + selectedNodeID: String?, + revealNodeID: String?, + revealRequestID: Int + ) { + self.roots = roots + self.expandedNodeIDs = expandedNodeIDs + self.selectedNodeID = selectedNodeID + let shouldReveal = revealRequestID != lastRevealRequestID && revealNodeID != nil + let preservedScrollY = shouldReveal ? nil : currentScrollY() + + let oldSignature = lastVisibleSignature + let newVisibleRows = flattenVisibleRows(from: roots, expandedNodeIDs: expandedNodeIDs) + let newSignature = newVisibleRows.map(\.node.id) + let structureChanged = newSignature != lastVisibleSignature + + visibleRows = newVisibleRows + + if structureChanged { + if !oldSignature.isEmpty, + let animations = rowAnimations(from: oldSignature, to: newSignature), + (!animations.removed.isEmpty || !animations.inserted.isEmpty) { + applyRowAnimations(removed: animations.removed, inserted: animations.inserted) + } else { + tableView.reloadData() + } + lastVisibleSignature = newSignature + } else { + refreshVisibleRows() + } + + tableView.deselectAll(nil) + + if let preservedScrollY { + restoreScrollPosition(y: preservedScrollY) + } + + if shouldReveal, let revealNodeID { + reveal(nodeID: revealNodeID) + lastRevealRequestID = revealRequestID + } + } + + func numberOfRows(in tableView: NSTableView) -> Int { + visibleRows.count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + guard visibleRows.indices.contains(row) else { return nil } + + let visibleRow = visibleRows[row] + let node = visibleRow.node + let identifier = NSUserInterfaceItemIdentifier("ExperimentalOutlineCell") + let cell = (tableView.makeView(withIdentifier: identifier, owner: nil) as? ExperimentalTableCellView) + ?? ExperimentalTableCellView(identifier: identifier) + + cell.configure(rootView: rowContent( + node, + expandedNodeIDs.contains(node.id), + visibleRow.depth, + 0, + { [weak self] in self?.activate(nodeID: node.id) } + )) + return cell + } + + func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { + ExperimentalClearRowView() + } + + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + guard visibleRows.indices.contains(row) else { return 25 } + if case .topSpacer(let height) = visibleRows[row].node.row { + return height + } + return 25 + } + + func tableViewSelectionDidChange(_ notification: Notification) { + tableView.deselectAll(nil) + } + + private func activate(nodeID: String) { + guard let node = findNode(id: nodeID, in: roots) else { return } + + if !node.children.isEmpty { + let shouldExpand = !expandedNodeIDs.contains(node.id) + onExpansionChanged(node, shouldExpand) + } + + onSelectionChanged(node) + onActivation(node) + } + + private func refreshVisibleRows() { + let visibleRange = tableView.rows(in: tableView.visibleRect) + guard visibleRange.length > 0 else { return } + + for row in visibleRange.location ..< (visibleRange.location + visibleRange.length) { + guard visibleRows.indices.contains(row), + let cell = tableView.view(atColumn: 0, row: row, makeIfNecessary: false) as? ExperimentalTableCellView + else { continue } + + let visibleRow = visibleRows[row] + let node = visibleRow.node + cell.configure(rootView: rowContent( + node, + expandedNodeIDs.contains(node.id), + visibleRow.depth, + 0, + { [weak self] in self?.activate(nodeID: node.id) } + )) + } + } + + private func rowAnimations( + from oldSignature: [String], + to newSignature: [String] + ) -> (removed: IndexSet, inserted: IndexSet)? { + let oldSet = Set(oldSignature) + let newSet = Set(newSignature) + + if oldSet.count != oldSignature.count || newSet.count != newSignature.count { + return nil + } + + let removed = IndexSet(oldSignature.enumerated().compactMap { index, id in + newSet.contains(id) ? nil : index + }) + let inserted = IndexSet(newSignature.enumerated().compactMap { index, id in + oldSet.contains(id) ? nil : index + }) + + return (removed, inserted) + } + + private func applyRowAnimations(removed: IndexSet, inserted: IndexSet) { + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.12 + tableView.beginUpdates() + if !removed.isEmpty { + tableView.removeRows(at: removed, withAnimation: [.slideUp]) + } + if !inserted.isEmpty { + tableView.insertRows(at: inserted, withAnimation: [.slideDown]) + } + tableView.endUpdates() + } + refreshVisibleRows() + } + + private func flattenVisibleRows( + from roots: [ExperimentalObjectBrowserNode], + expandedNodeIDs: Set + ) -> [VisibleRow] { + var rows: [VisibleRow] = [] + + func append(nodes: [ExperimentalObjectBrowserNode], depth: Int) { + for node in nodes { + rows.append(VisibleRow(node: node, depth: depth)) + guard expandedNodeIDs.contains(node.id) else { continue } + append(nodes: node.children, depth: childDepth(for: node, currentDepth: depth)) + } + } + + append(nodes: roots, depth: 0) + return rows + } + + private func childDepth(for node: ExperimentalObjectBrowserNode, currentDepth: Int) -> Int { + switch node.row { + case .topSpacer: + currentDepth + case .pendingConnection: + currentDepth + case .server: + currentDepth + case .databasesFolder, .serverFolder, .databaseFolder, .databaseSubfolder, .securitySection: + currentDepth + 1 + case .database, .objectGroup, .action, .infoLeaf, .loading, .message, .object, + .agentJob, .databaseSnapshot, .linkedServer, .ssisFolder, .serverTrigger, + .securityLogin, .securityServerRole, .securityCredential, .databaseNamedItem: + currentDepth + 1 + } + } + + private func findNode( + id: String, + in nodes: [ExperimentalObjectBrowserNode] + ) -> ExperimentalObjectBrowserNode? { + for node in nodes { + if node.id == id { + return node + } + if let child = findNode(id: id, in: node.children) { + return child + } + } + return nil + } + + private func reveal(nodeID: String) { + guard let row = visibleRows.firstIndex(where: { $0.node.id == nodeID }) else { return } + let targetRow = preferredRevealRow(for: row) + scrollRowToTop(targetRow) + } + + private func preferredRevealRow(for row: Int) -> Int { + guard row > 0 else { return row } + if case .topSpacer = visibleRows[row - 1].node.row { + return row - 1 + } + return row + } + + private func currentScrollY() -> CGFloat? { + tableView.enclosingScrollView?.contentView.bounds.origin.y + } + + private func restoreScrollPosition(y: CGFloat) { + guard let scrollView = tableView.enclosingScrollView else { return } + let clipView = scrollView.contentView + let maxY = max(0, tableView.bounds.height - clipView.bounds.height) + let clampedY = min(max(0, y), maxY) + clipView.scroll(to: NSPoint(x: 0, y: clampedY)) + scrollView.reflectScrolledClipView(clipView) + } + + private func scrollRowToTop(_ row: Int) { + guard let scrollView = tableView.enclosingScrollView, row >= 0, row < tableView.numberOfRows else { + return + } + let clipView = scrollView.contentView + let rowRect = tableView.rect(ofRow: row) + let maxY = max(0, tableView.bounds.height - clipView.bounds.height) + let targetY = min(max(0, rowRect.minY), maxY) + clipView.scroll(to: NSPoint(x: 0, y: targetY)) + scrollView.reflectScrolledClipView(clipView) + } + } +} + +@MainActor +final class ExperimentalTableCellView: NSTableCellView { + private let hostingView = NSHostingView(rootView: AnyView(EmptyView())) + + init(identifier: NSUserInterfaceItemIdentifier) { + super.init(frame: .zero) + self.identifier = identifier + + hostingView.translatesAutoresizingMaskIntoConstraints = false + addSubview(hostingView) + + NSLayoutConstraint.activate([ + hostingView.leadingAnchor.constraint(equalTo: leadingAnchor), + hostingView.trailingAnchor.constraint(equalTo: trailingAnchor), + hostingView.topAnchor.constraint(equalTo: topAnchor), + hostingView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError() + } + + func configure(rootView: AnyView) { + hostingView.rootView = rootView + } +} + +@MainActor +final class ExperimentalClearRowView: NSTableRowView { + override func drawSelection(in dirtyRect: NSRect) {} + override func drawBackground(in dirtyRect: NSRect) {} +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserRowView+Helpers.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserRowView+Helpers.swift new file mode 100644 index 000000000..d4d9215b6 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserRowView+Helpers.swift @@ -0,0 +1,75 @@ +import SwiftUI + +extension ExperimentalObjectBrowserRowView { + func serverDisplayName(_ connection: SavedConnection) -> String { + let name = connection.connectionName.trimmingCharacters(in: .whitespacesAndNewlines) + return name.isEmpty ? connection.host : name + } + + func serverDisplayName(_ session: ConnectionSession) -> String { + serverDisplayName(session.connection) + } + + func serverSubtitle(_ session: ConnectionSession) -> String { + if let version = serverVersionLabel(session) { + return "\(session.connection.databaseType.displayName) (\(version))" + } + return session.connection.databaseType.displayName + } + + func serverVersionLabel(_ session: ConnectionSession) -> String? { + let raw = session.databaseStructure?.serverVersion ?? session.connection.serverVersion + guard let raw, !raw.isEmpty else { return nil } + let prefixes = ["SQL Server ", "PostgreSQL ", "Microsoft SQL Server "] + for prefix in prefixes where raw.hasPrefix(prefix) { + let version = String(raw.dropFirst(prefix.count)) + return version.isEmpty ? nil : version + } + if ["PostgreSQL", "Microsoft SQL Server", "SQL Server"].contains(raw) { + return nil + } + return raw + } + + func databaseIconColor(_ database: DatabaseInfo, session: ConnectionSession) -> Color { + if !database.isOnline || !database.isAccessible { + return ColorTokens.Text.quaternary + } + if isSelected { + return resolvedAccentColor(for: session.connection) + } + return projectStore.globalSettings.sidebarIconColorMode == .colorful + ? ExplorerSidebarPalette.databaseInstance + : ExplorerSidebarPalette.monochrome + } + + func resolvedAccentColor(for connection: SavedConnection) -> Color { + switch projectStore.globalSettings.accentColorSource { + case .system: + Color.accentColor + case .connection: + connection.color + case .custom: + ColorTokens.accent + } + } + + func objectIconName(_ type: SchemaObjectInfo.ObjectType) -> String { + switch type { + case .table: "tablecells" + case .view, .materializedView: "eye" + case .function: "function" + case .trigger: "bolt" + case .procedure: "terminal" + case .extension: "puzzlepiece" + case .sequence: "number" + case .type: "t.square" + case .synonym: "arrow.triangle.branch" + } + } + + func objectSubtitle(_ object: SchemaObjectInfo) -> String? { + guard object.type == .trigger, let table = object.triggerTable, !table.isEmpty else { return nil } + return "on \(table)" + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserRowView.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserRowView.swift new file mode 100644 index 000000000..395e050a7 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserRowView.swift @@ -0,0 +1,583 @@ +import SwiftUI +import AppKit +import SQLServerKit + +struct ExperimentalObjectBrowserRowView: View { + let node: ExperimentalObjectBrowserNode + let isExpanded: Bool + let isSelected: Bool + let outlineLevel: Int + let outlineOffset: CGFloat + let isHighlighted: Bool + let highlightPulse: Bool + let contextMenuBuilder: (() -> NSMenu?)? + let onActivate: () -> Void + + @Environment(ProjectStore.self) var projectStore + @Environment(EnvironmentState.self) var environmentState + + private var depth: Int { + max(0, outlineLevel) + } + + private var leadingAlignmentCompensation: CGFloat { + switch node.row { + case .topSpacer: + 0 + case .server: + -(SidebarRowConstants.rowOuterHorizontalPadding + SpacingTokens.xs) + case .pendingConnection: + -(SidebarRowConstants.rowOuterHorizontalPadding + SpacingTokens.xs) + default: + -(SidebarRowConstants.rowOuterHorizontalPadding + SpacingTokens.xxxs) + } + } + + var body: some View { + rowBody + .padding(.leading, leadingAlignmentCompensation) + .overlay { + if shouldShowHighlightOverlay { + StatusWaveOverlay( + color: ColorTokens.Status.success, + cornerRadius: SidebarRowConstants.hoverCornerRadius, + trigger: highlightPulse + ) + .clipShape(RoundedRectangle(cornerRadius: SidebarRowConstants.hoverCornerRadius, style: .continuous)) + .allowsHitTesting(false) + } + } + .modifier(RowLazyContextMenu(menuBuilder: contextMenuBuilder)) + } + + private var shouldShowHighlightOverlay: Bool { + guard isHighlighted else { return false } + switch node.row { + case .topSpacer, .pendingConnection, .server: + return false + default: + return true + } + } + + @ViewBuilder + private var rowBody: some View { + switch node.row { + case .topSpacer(let height): + Color.clear + .frame(height: height) + case .pendingConnection(let pending): + pendingConnectionRow(pending: pending) + case .server(let session): + serverRow(session: session) + case .databasesFolder(_, let count): + buttonRow { + SidebarRow( + depth: depth, + icon: .system("cylinder"), + label: "Databases", + isExpanded: Binding(get: { isExpanded }, set: { _ in onActivate() }), + iconColor: ExplorerSidebarPalette.folderIconColor( + title: "Databases", + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + ) { + Text("\(count)") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } + case .database(let session, let database, let isLoading): + buttonRow { + SidebarRow( + depth: depth, + icon: .system("internaldrive"), + label: database.name, + isExpanded: Binding(get: { isExpanded }, set: { _ in onActivate() }), + isSelected: isSelected, + iconColor: databaseIconColor(database, session: session), + labelColor: database.isAccessible ? ColorTokens.Text.primary : ColorTokens.Text.secondary, + accentColor: resolvedAccentColor(for: session.connection) + ) { + if !database.isOnline, let state = database.stateDescription { + Text(state.uppercased()) + .font(TypographyTokens.compact) + .foregroundStyle(ColorTokens.Text.quaternary) + } else if !database.isAccessible { + Text("NO ACCESS") + .font(TypographyTokens.compact) + .foregroundStyle(ColorTokens.Text.quaternary) + } + if isLoading { + ProgressView() + .controlSize(.mini) + } + } + .opacity(database.isOnline && database.isAccessible ? 1 : 0.5) + } + case .objectGroup(_, _, let type, let count): + buttonRow { + SidebarRow( + depth: depth, + icon: .system(type.systemImage), + label: type.pluralDisplayName, + isExpanded: Binding(get: { isExpanded }, set: { _ in onActivate() }), + iconColor: ExplorerSidebarPalette.objectGroupIconColor( + for: type, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + ) { + Text("\(count)") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } + case .object(let session, _, let object): + buttonRow { + SidebarRow( + depth: depth, + icon: .system(objectIconName(object.type)), + label: object.fullName, + subtitle: objectSubtitle(object), + isSelected: isSelected, + iconColor: ExplorerSidebarPalette.objectGroupIconColor( + for: object.type, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + accentColor: resolvedAccentColor(for: session.connection) + ) + } + case .serverFolder(_, let kind, let count): + buttonRow { + SidebarRow( + depth: depth, + icon: .system(kind.systemImage), + label: kind.title, + isExpanded: Binding(get: { isExpanded }, set: { _ in onActivate() }), + iconColor: ExplorerSidebarPalette.folderIconColor( + title: kind.title, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + ) { + if let count { + Text("\(count)") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } + } + case .databaseFolder(_, _, let kind, let count, let isLoading): + buttonRow { + SidebarRow( + depth: depth, + icon: .system(kind.systemImage), + label: kind.title, + isExpanded: Binding(get: { isExpanded }, set: { _ in onActivate() }), + iconColor: ExplorerSidebarPalette.folderIconColor( + title: kind.title, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + ) { + if let count { + Text("\(count)") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + } + if isLoading { + ProgressView() + .controlSize(.mini) + } + } + } + case .databaseSubfolder(_, _, let title, let systemImage, let paletteTitle, let count): + buttonRow { + SidebarRow( + depth: depth, + icon: .system(systemImage), + label: title, + isExpanded: Binding(get: { isExpanded }, set: { _ in onActivate() }), + iconColor: ExplorerSidebarPalette.folderIconColor( + title: paletteTitle, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + ) { + if let count { + Text("\(count)") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } + } + case .databaseNamedItem(let session, _, let title, let systemImage, let paletteTitle, let detail): + buttonRow { + SidebarRow( + depth: depth, + icon: .system(systemImage), + label: title, + isSelected: isSelected, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: paletteTitle, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + accentColor: resolvedAccentColor(for: session.connection) + ) { + if let detail, !detail.isEmpty { + Text(detail) + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + .lineLimit(1) + } + } + } + case .securitySection(_, let kind, let count, let isLoading): + buttonRow { + SidebarRow( + depth: depth, + icon: .system(kind.systemImage), + label: kind.title, + isExpanded: Binding(get: { isExpanded }, set: { _ in onActivate() }), + iconColor: ExplorerSidebarPalette.folderIconColor( + title: kind.title, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + ) { + Text("\(count)") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + if isLoading { + ProgressView() + .controlSize(.mini) + } + } + } + case .securityLogin(_, let login): + SidebarRow( + depth: depth, + icon: .system(securityLoginIconName(login)), + label: login.name, + iconColor: securityLoginIconColor(login), + labelColor: login.isDisabled ? ColorTokens.Text.secondary : ColorTokens.Text.primary + ) { + Text(login.loginType) + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + + if login.isDisabled { + Text("Disabled") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.quaternary) + } + } + case .securityServerRole(_, let role): + SidebarRow( + depth: depth, + icon: .system("shield"), + label: role.name, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: "Server Roles", + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + ) { + if role.isFixed { + Text("Fixed") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.quaternary) + } + } + case .securityCredential(_, let credential): + SidebarRow( + depth: depth, + icon: .system("key"), + label: credential.name, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: "Credentials", + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + ) { + Text(credential.identity) + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + .lineLimit(1) + } + case .agentJob(_, let job): + buttonRow { + SidebarRow( + depth: depth, + icon: .system("clock"), + label: job.name, + isSelected: isSelected, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: "Agent Jobs", + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + accentColor: Color.accentColor + ) { + if let lastOutcome = job.lastOutcome, !lastOutcome.isEmpty { + Text(lastOutcome) + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } + } + case .databaseSnapshot(_, let snapshot): + buttonRow { + SidebarRow( + depth: depth, + icon: .system("camera.fill"), + label: snapshot.name, + isSelected: isSelected, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: "Database Snapshots", + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + accentColor: Color.accentColor + ) { + Text(snapshot.sourceDatabaseName) + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + .lineLimit(1) + } + } + case .linkedServer(_, let server): + buttonRow { + SidebarRow( + depth: depth, + icon: .system("link"), + label: server.name, + isSelected: isSelected, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: "Linked Servers", + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + labelColor: server.isDataAccessEnabled ? ColorTokens.Text.primary : ColorTokens.Text.secondary, + accentColor: Color.accentColor + ) { + if !server.dataSource.isEmpty { + Text(server.dataSource) + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + .lineLimit(1) + } + } + } + case .ssisFolder(_, let folder): + buttonRow { + SidebarRow( + depth: depth, + icon: .system("folder"), + label: folder.name, + isSelected: isSelected, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: "Integration Services Catalogs", + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + accentColor: Color.accentColor + ) + } + case .serverTrigger(_, let trigger): + buttonRow { + SidebarRow( + depth: depth, + icon: .system("bolt"), + label: trigger.name, + isSelected: isSelected, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: "Server Triggers", + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + labelColor: trigger.isDisabled ? ColorTokens.Text.tertiary : ColorTokens.Text.primary, + accentColor: Color.accentColor + ) { + if trigger.isDisabled { + Text("Disabled") + .font(SidebarRowConstants.trailingFont) + .foregroundStyle(ColorTokens.Text.tertiary) + } + } + } + case .action(_, let action, _): + buttonRow { + SidebarRow( + depth: depth, + icon: .system(action.systemImage), + label: action.title, + isSelected: isSelected, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: action.title, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + accentColor: Color.accentColor + ) + } + case .infoLeaf(let title, let systemImage, let paletteTitle, _): + SidebarRow( + depth: depth, + icon: .system(systemImage), + label: title, + iconColor: ExplorerSidebarPalette.folderIconColor( + title: paletteTitle, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ), + labelColor: ColorTokens.Text.secondary, + labelFont: TypographyTokens.detail + ) + case .loading(let title, _): + SidebarRow( + depth: depth, + icon: .none, + label: title, + labelColor: ColorTokens.Text.tertiary, + labelFont: TypographyTokens.detail + ) { + ProgressView() + .controlSize(.mini) + } + case .message(let title, let systemImage, _): + SidebarRow( + depth: depth, + icon: .system(systemImage), + label: title, + iconColor: ColorTokens.Status.warning, + labelColor: ColorTokens.Text.secondary, + labelFont: TypographyTokens.detail + ) + } + } + + private func serverRow(session: ConnectionSession) -> some View { + SidebarConnectionHeader( + connectionName: serverDisplayName(session), + subtitle: serverSubtitle(session), + databaseType: session.connection.databaseType, + connectionColor: resolvedAccentColor(for: session.connection), + isExpanded: Binding(get: { isExpanded }, set: { _ in onActivate() }), + isColorful: projectStore.globalSettings.sidebarIconColorMode == .colorful, + isSecure: session.connection.useTLS, + connectionState: session.connectionState, + onAction: onActivate, + iconScale: 1, + iconFrameScale: 1.58, + iconGlyphScale: 1.55, + leadingPaddingAdjustment: -SpacingTokens.xxs2, + statusPresentation: .none, + labelFont: TypographyTokens.standard.weight(.medium) + ) + .overlay { + if isHighlighted { + StatusWaveOverlay( + color: ColorTokens.Status.success, + cornerRadius: SidebarRowConstants.hoverCornerRadius, + trigger: highlightPulse + ) + } + } + } + + private func pendingConnectionRow(pending: PendingConnection) -> some View { + let connection = pending.connection + let connectionState: ConnectionState = switch pending.phase { + case .connecting: + .connecting + case .failed(let message): + .error(.connectionFailed(message)) + } + + let trailingAccessory: SidebarConnectionHeader.TrailingAccessory = switch pending.phase { + case .connecting: + .spinner + case .failed: + .retryButton({ + environmentState.retryPendingConnection(for: connection.id) + }) + } + + return SidebarConnectionHeader( + connectionName: serverDisplayName(connection), + subtitle: connection.databaseType.displayName, + databaseType: connection.databaseType, + connectionColor: resolvedAccentColor(for: connection), + isExpanded: .constant(false), + isColorful: projectStore.globalSettings.sidebarIconColorMode == .colorful, + isSecure: connection.useTLS, + connectionState: connectionState, + onAction: {}, + trailingAccessory: trailingAccessory, + iconScale: 1, + iconFrameScale: 1.58, + iconGlyphScale: 1.55, + leadingPaddingAdjustment: -SpacingTokens.xxs2, + statusPresentation: .none, + labelFont: TypographyTokens.standard.weight(.medium) + ) + .background { + switch pending.phase { + case .connecting: + StatusWaveOverlay( + color: ColorTokens.accent, + cornerRadius: SidebarRowConstants.hoverCornerRadius, + continuous: true + ) + .clipShape(RoundedRectangle(cornerRadius: SidebarRowConstants.hoverCornerRadius, style: .continuous)) + .allowsHitTesting(false) + case .failed: + StatusWaveOverlay( + color: ColorTokens.Status.error, + cornerRadius: SidebarRowConstants.hoverCornerRadius, + trigger: true + ) + .clipShape(RoundedRectangle(cornerRadius: SidebarRowConstants.hoverCornerRadius, style: .continuous)) + .allowsHitTesting(false) + } + } + } + + private func securityLoginIconName( + _ login: ExperimentalObjectBrowserSidebarViewModel.SecurityLoginItem + ) -> String { + if login.loginType == "Group Role" { + return "person.2.circle" + } + return login.isDisabled ? "person.crop.circle.badge.xmark" : "person.crop.circle" + } + + private func securityLoginIconColor( + _ login: ExperimentalObjectBrowserSidebarViewModel.SecurityLoginItem + ) -> Color { + if login.isDisabled { + return ColorTokens.Text.quaternary + } + let title: String = if login.loginType == "Group Role" { + "Group Roles" + } else if login.loginType.contains("Login") || login.loginType.contains("Superuser") { + "Login Roles" + } else { + "Logins" + } + return ExplorerSidebarPalette.folderIconColor( + title: title, + colored: projectStore.globalSettings.sidebarIconColorMode == .colorful + ) + } + + private func buttonRow(@ViewBuilder content: () -> Content) -> some View { + Button(action: onActivate) { + content() + } + .buttonStyle(.plain) + .animation(.snappy(duration: 0.18, extraBounce: 0), value: isExpanded) + } +} + +private struct RowLazyContextMenu: ViewModifier { + let menuBuilder: (() -> NSMenu?)? + + func body(content: Content) -> some View { + if let menuBuilder { + content.lazyContextMenu { + menuBuilder() ?? NSMenu() + } + } else { + content + } + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+ContextMenus.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+ContextMenus.swift new file mode 100644 index 000000000..e0f069339 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+ContextMenus.swift @@ -0,0 +1,1368 @@ +import AppKit +import SwiftUI +import SQLServerKit + +extension ExperimentalObjectBrowserSidebarView { + func contextMenu(for node: ExperimentalObjectBrowserNode) -> NSMenu? { + switch node.row { + case .topSpacer: + nil + case .pendingConnection(let pending): + pendingConnectionMenu(for: pending) + case .server(let session): + serverMenu(for: session) + case .databasesFolder(let session, _): + databasesFolderMenu(for: session) + case .database(let session, let database, _): + databaseMenu(for: database, session: session) + case .objectGroup(let session, let databaseName, let type, _): + objectGroupMenu(for: type, databaseName: databaseName, session: session) + case .object(let session, let databaseName, let object): + objectMenu(for: object, databaseName: databaseName, session: session) + case .serverFolder(let session, let kind, _): + serverFolderMenu(kind: kind, session: session) + case .databaseFolder(let session, let databaseName, let kind, _, _): + databaseFolderMenu(kind: kind, databaseName: databaseName, session: session) + case .databaseSubfolder(let session, let databaseName, let title, _, _, _): + databaseSubfolderMenu(title: title, databaseName: databaseName, session: session) + case .databaseNamedItem: + nil + case .securitySection(let session, let kind, _, _): + securitySectionMenu(kind: kind, session: session) + case .securityLogin(let session, let login): + securityLoginMenu(login: login, session: session) + case .securityServerRole(let session, let role): + securityServerRoleMenu(role: role, session: session) + case .securityCredential(let session, let credential): + securityCredentialMenu(credential: credential, session: session) + case .databaseSnapshot(let session, let snapshot): + snapshotMenu(snapshot: snapshot, session: session) + case .linkedServer(let session, let server): + linkedServerMenu(server: server, session: session) + case .serverTrigger(let session, let trigger): + serverTriggerMenu(trigger: trigger, session: session) + case .agentJob(let session, _): + agentJobMenu(for: session) + case .ssisFolder, .action, .infoLeaf, .loading, .message: + nil + } + } + + private func pendingConnectionMenu(for pending: PendingConnection) -> NSMenu { + let menu = NSMenu() + + switch pending.phase { + case .connecting: + menu.addActionItem("Cancel Connection", systemImage: "xmark.circle") { + environmentState.cancelPendingConnection(for: pending.connection.id) + } + menu.addActionItem("Edit Connection", systemImage: "pencil") { + ManageConnectionsWindowController.shared.present() + } + case .failed: + menu.addActionItem("Retry", systemImage: "arrow.clockwise") { + environmentState.retryPendingConnection(for: pending.connection.id) + } + menu.addActionItem("Edit Connection", systemImage: "pencil") { + ManageConnectionsWindowController.shared.present() + } + menu.addDivider() + menu.addActionItem("Remove", systemImage: "trash") { + environmentState.removePendingConnection(for: pending.connection.id) + } + } + + return menu + } + + private func serverFolderMenu( + kind: ExperimentalObjectBrowserServerFolderKind, + session: ConnectionSession + ) -> NSMenu? { + let menu = NSMenu() + + switch kind { + case .security: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + Task { + let handle = AppDirector.shared.activityEngine.begin("Refreshing security", connectionSessionID: session.id) + await loadServerSecurityAsync(session: session) + handle.succeed() + } + } + switch session.connection.databaseType { + case .microsoftSQL: + menu.addActionItem("New Login", systemImage: "person.badge.plus") { + let value = environmentState.prepareLoginEditorWindow( + connectionSessionID: session.connection.id, + existingLogin: nil + ) + openWindow(id: LoginEditorWindow.sceneID, value: value) + } + menu.addDivider() + menu.addActionItem("Open Security Management", systemImage: "lock.shield") { + environmentState.openServerSecurityTab(connectionID: session.connection.id) + } + case .postgresql: + menu.addActionItem("New Login Role", systemImage: "person.badge.plus") { + sheetState.securityPGRoleSheetSessionID = session.connection.id + sheetState.securityPGRoleSheetEditName = nil + sheetState.showSecurityPGRoleSheet = true + } + menu.addActionItem("New Group Role", systemImage: "person.2.badge.plus") { + sheetState.securityPGRoleSheetSessionID = session.connection.id + sheetState.securityPGRoleSheetEditName = nil + sheetState.showSecurityPGRoleSheet = true + } + case .mysql, .sqlite: + break + } + case .agentJobs: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + loadAgentJobs(session: session) + } + menu.addDivider() + menu.addActionItem("Open in Tab", systemImage: "list.bullet.rectangle") { + environmentState.openJobQueueTab(for: session) + } + menu.addActionItem("Open in New Window", systemImage: "rectangle.portrait.and.arrow.right") { + let sessionID = environmentState.prepareJobQueueWindow(for: session) + openWindow(id: JobQueueWindow.sceneID, value: sessionID) + } + case .databaseSnapshots: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + loadDatabaseSnapshots(session: session) + } + menu.addDivider() + menu.addActionItem("New Snapshot", systemImage: "camera.badge.ellipsis") { + sheetState.createSnapshotConnectionID = session.connection.id + sheetState.showCreateSnapshotSheet = true + } + case .ssis: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + Task { await loadSSISFoldersAsync(session: session) } + } + case .linkedServers: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + loadLinkedServers(session: session) + } + menu.addDivider() + menu.addActionItem("New Linked Server", systemImage: "link.badge.plus") { + sheetState.newLinkedServerSessionID = session.connection.id + sheetState.showNewLinkedServerSheet = true + } + case .serverTriggers: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + loadServerTriggers(session: session) + } + menu.addActionItem("New Server Trigger", systemImage: "bolt") { + sheetState.newServerTriggerConnectionID = session.connection.id + sheetState.showNewServerTriggerSheet = true + } + case .management: + return nil + } + + return menu + } + + private func securitySectionMenu( + kind: ExperimentalObjectBrowserSecuritySectionKind, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + Task { + let handle = AppDirector.shared.activityEngine.begin("Refreshing \(kind.title.lowercased())", connectionSessionID: session.id) + await loadServerSecurityAsync(session: session) + handle.succeed() + } + } + + switch kind { + case .logins: + menu.addActionItem("New Login", systemImage: "person.badge.plus") { + let value = environmentState.prepareLoginEditorWindow( + connectionSessionID: session.connection.id, + existingLogin: nil + ) + openWindow(id: LoginEditorWindow.sceneID, value: value) + } + case .serverRoles: + menu.addActionItem("New Server Role", systemImage: "person.2.badge.plus") { + createMSSQLServerRole(session: session) + } + case .credentials: + menu.addActionItem("New Credential", systemImage: "key.fill") { + createMSSQLCredential(session: session) + } + case .pgLoginRoles: + menu.addActionItem("New Login Role", systemImage: "person.badge.plus") { + sheetState.securityPGRoleSheetSessionID = session.connection.id + sheetState.securityPGRoleSheetEditName = nil + sheetState.showSecurityPGRoleSheet = true + } + case .pgGroupRoles: + menu.addActionItem("New Group Role", systemImage: "person.2.badge.plus") { + sheetState.securityPGRoleSheetSessionID = session.connection.id + sheetState.securityPGRoleSheetEditName = nil + sheetState.showSecurityPGRoleSheet = true + } + case .certificateLogins: + break + } + + return menu + } + + private func securityLoginMenu( + login: ExperimentalObjectBrowserSidebarViewModel.SecurityLoginItem, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + + if session.connection.databaseType == .postgresql { + menu.addActionItem("Reassign Owned Objects", systemImage: "arrow.triangle.swap") { + Task { await reassignPGRole(name: login.name, session: session) } + } + menu.addDivider() + menu.addSubmenu("Script as", systemImage: "scroll") { sub in + let loginAttribute = login.loginType.contains("Login") || login.loginType.contains("Superuser") ? " LOGIN" : "" + sub.addActionItem("CREATE", systemImage: "plus.rectangle.on.rectangle") { + openScriptTab(sql: "CREATE ROLE \"\(login.name)\"\(loginAttribute);", session: session) + } + sub.addDivider() + sub.addActionItem("DROP", systemImage: "trash") { + openScriptTab(sql: "DROP ROLE \"\(login.name)\";", session: session) + } + } + menu.addDivider() + menu.addActionItem("Drop Role", systemImage: "trash") { + sheetState.dropSecurityPrincipalTarget = .init( + sessionID: session.id, + connectionID: session.connection.id, + name: login.name, + kind: .pgRole, + databaseName: nil + ) + sheetState.showDropSecurityPrincipalAlert = true + } + menu.addDivider() + menu.addActionItem("Properties", systemImage: "info.circle") { + sheetState.securityPGRoleSheetSessionID = session.connection.id + sheetState.securityPGRoleSheetEditName = login.name + sheetState.showSecurityPGRoleSheet = true + } + return menu + } + + menu.addSubmenu("Script as", systemImage: "scroll") { sub in + let createSQL = if login.loginType == "SQL" { + "CREATE LOGIN [\(login.name)] WITH PASSWORD = N'';" + } else { + "CREATE LOGIN [\(login.name)] FROM WINDOWS;" + } + sub.addActionItem("CREATE", systemImage: "plus.rectangle.on.rectangle") { + openScriptTab(sql: createSQL, session: session) + } + sub.addDivider() + sub.addActionItem("DROP", systemImage: "trash") { + openScriptTab(sql: "DROP LOGIN [\(login.name)];", session: session) + } + } + menu.addDivider() + if login.isDisabled { + menu.addActionItem("Enable Login", systemImage: "checkmark.circle") { + Task { await enableMSSQLLogin(name: login.name, enabled: true, session: session) } + } + } else { + menu.addActionItem("Disable Login", systemImage: "nosign") { + Task { await enableMSSQLLogin(name: login.name, enabled: false, session: session) } + } + } + menu.addDivider() + menu.addActionItem("Drop Login", systemImage: "trash") { + sheetState.dropSecurityPrincipalTarget = .init( + sessionID: session.id, + connectionID: session.connection.id, + name: login.name, + kind: .mssqlLogin, + databaseName: nil + ) + sheetState.showDropSecurityPrincipalAlert = true + } + menu.addDivider() + menu.addActionItem("Properties", systemImage: "info.circle") { + let value = environmentState.prepareLoginEditorWindow( + connectionSessionID: session.connection.id, + existingLogin: login.name + ) + openWindow(id: LoginEditorWindow.sceneID, value: value) + } + return menu + } + + private func securityServerRoleMenu( + role: ExperimentalObjectBrowserSidebarViewModel.SecurityServerRoleItem, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + menu.addActionItem("List Members", systemImage: "person.2") { + openScriptTab( + sql: """ + SELECT m.name AS member_name, m.type_desc + FROM sys.server_role_members rm + JOIN sys.server_principals r ON rm.role_principal_id = r.principal_id + JOIN sys.server_principals m ON rm.member_principal_id = m.principal_id + WHERE r.name = N'\(role.name)'; + """, + session: session + ) + } + + if !role.isFixed { + menu.addDivider() + menu.addSubmenu("Script as", systemImage: "scroll") { sub in + sub.addActionItem("CREATE", systemImage: "plus.rectangle.on.rectangle") { + openScriptTab(sql: "CREATE SERVER ROLE [\(role.name)];", session: session) + } + sub.addDivider() + sub.addActionItem("DROP", systemImage: "trash") { + openScriptTab(sql: "DROP SERVER ROLE [\(role.name)];", session: session) + } + } + menu.addDivider() + menu.addActionItem("Drop Server Role", systemImage: "trash") { + sheetState.dropSecurityPrincipalTarget = .init( + sessionID: session.id, + connectionID: session.connection.id, + name: role.name, + kind: .mssqlServerRole, + databaseName: nil + ) + sheetState.showDropSecurityPrincipalAlert = true + } + } + + return menu + } + + private func securityCredentialMenu( + credential: ExperimentalObjectBrowserSidebarViewModel.SecurityCredentialItem, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + menu.addSubmenu("Script as", systemImage: "scroll") { sub in + sub.addActionItem("CREATE", systemImage: "plus.rectangle.on.rectangle") { + openScriptTab( + sql: "CREATE CREDENTIAL [\(credential.name)] WITH IDENTITY = N'\(credential.identity)', SECRET = N'';", + session: session + ) + } + sub.addDivider() + sub.addActionItem("DROP", systemImage: "trash") { + openScriptTab(sql: "DROP CREDENTIAL [\(credential.name)];", session: session) + } + } + return menu + } + + private func serverMenu(for session: ConnectionSession) -> NSMenu { + let menu = NSMenu() + + menu.addActionItem("Refresh All", systemImage: "arrow.clockwise") { + Task { + let handle = AppDirector.shared.activityEngine.begin("Refreshing all databases", connectionSessionID: session.id) + await environmentState.refreshDatabaseStructure(for: session.id, scope: .full) + handle.succeed() + } + } + menu.addActionItem("New Query", systemImage: "doc.text") { + environmentState.openQueryTab(for: session) + } + menu.addDivider() + menu.addActionItem("Activity Monitor", systemImage: "gauge.with.dots.needle.33percent") { + environmentState.openActivityMonitorTab(connectionID: session.connection.id) + } + menu.addDivider() + menu.addActionItem("Maintenance", systemImage: "wrench.and.screwdriver") { + environmentState.openMaintenanceTab(connectionID: session.connection.id) + } + + if session.connection.databaseType == .microsoftSQL { + menu.addActionItem("Database Mail", systemImage: "envelope") { + let value = environmentState.prepareDatabaseMailEditorWindow(connectionSessionID: session.connection.id) + openWindow(id: DatabaseMailEditorWindow.sceneID, value: value) + } + menu.addActionItem("Central Management Servers", systemImage: "server.rack") { + sheetState.cmsConnectionID = session.connection.id + sheetState.showCMSSheet = true + } + menu.addActionItem("Extended Events", systemImage: "waveform.path.ecg") { + environmentState.openActivityMonitorTab(connectionID: session.connection.id, section: "XEvents") + } + menu.addActionItem("Availability Groups", systemImage: "server.rack") { + environmentState.openAvailabilityGroupsTab(connectionID: session.connection.id) + } + menu.addDivider() + + let connID = session.connection.id + let hideOffline = viewModel.hideOfflineDatabasesBySession[connID] ?? false + let item = menu.addActionItem("Hide Offline Databases", systemImage: "eye.slash") { + viewModel.hideOfflineDatabasesBySession[connID] = !(viewModel.hideOfflineDatabasesBySession[connID] ?? false) + } + item.state = hideOffline ? NSControl.StateValue.on : NSControl.StateValue.off + } + + menu.addDivider() + menu.addActionItem("Manage Connection", systemImage: "slider.horizontal.3") { + ManageConnectionsWindowController.shared.present( + initialSection: .connections, + selectedConnectionID: session.connection.id + ) + } + menu.addActionItem("Disconnect", systemImage: "xmark.circle") { + Task { await environmentState.disconnectSession(withID: session.id) } + } + + menu.addDivider() + if session.connection.databaseType == .microsoftSQL { + menu.addActionItem("Properties", systemImage: "info.circle") { + let value = environmentState.prepareServerEditorWindow(connectionSessionID: session.connection.id) + openWindow(id: ServerEditorWindow.sceneID, value: value) + } + } else if session.connection.databaseType == .mysql { + menu.addActionItem("Server Properties", systemImage: "info.circle") { + environmentState.openServerPropertiesTab(connectionID: session.connection.id) + } + } + + return menu + } + + private func databasesFolderMenu(for session: ConnectionSession) -> NSMenu { + let menu = NSMenu() + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + Task { + let handle = AppDirector.shared.activityEngine.begin("Refreshing databases", connectionSessionID: session.id) + await environmentState.refreshDatabaseStructure(for: session.id, scope: .full) + handle.succeed() + } + } + let newDatabaseItem = menu.addActionItem("New Database", systemImage: "cylinder") { + sheetState.newDatabaseConnectionID = session.connection.id + sheetState.showNewDatabaseSheet = true + } + newDatabaseItem.isEnabled = session.permissions?.canCreateDatabases ?? true + if session.connection.databaseType == .microsoftSQL || session.connection.databaseType == .sqlite { + menu.addDivider() + menu.addActionItem("Attach Database", systemImage: "externaldrive.badge.plus") { + sheetState.attachConnectionID = session.connection.id + sheetState.showAttachSheet = true + } + } + return menu + } + + private func databaseMenu(for database: DatabaseInfo, session: ConnectionSession) -> NSMenu { + let menu = NSMenu() + let connID = session.connection.id + let dbType = session.connection.databaseType + + menu.addActionItem("Refresh Schema", systemImage: "arrow.clockwise") { + viewModel.setExpanded(true, nodeID: ExperimentalObjectBrowserSidebarViewModel.databaseNodeID(connectionID: connID, databaseName: database.name)) + Task { + let handle = AppDirector.shared.activityEngine.begin("Refreshing schema for \(database.name)", connectionSessionID: session.id) + await environmentState.loadSchemaForDatabase(database.name, connectionSession: session) + handle.succeed() + } + } + menu.addActionItem("New Query", systemImage: "doc.text") { + environmentState.openQueryTab(for: session, database: database.name) + } + + menu.addDivider() + + if dbType == .postgresql, projectStore.globalSettings.managedPostgresConsoleEnabled { + menu.addActionItem("Postgres Console", systemImage: "terminal") { + environmentState.openPSQLTab(for: session, database: database.name) + } + menu.addDivider() + } + + menu.addActionItem("Maintenance", systemImage: "wrench.and.screwdriver") { + environmentState.openMaintenanceTab(connectionID: connID, databaseName: database.name) + } + + if dbType == .postgresql { + menu.addSubmenu("Tasks", systemImage: "gearshape") { sub in + sub.addActionItem("Back Up", systemImage: "arrow.down.doc") { + sheetState.pgBackupDatabaseName = database.name + sheetState.pgBackupConnectionID = connID + sheetState.showPgBackupSheet = true + } + sub.addActionItem("Restore", systemImage: "arrow.up.doc") { + sheetState.pgBackupDatabaseName = database.name + sheetState.pgBackupConnectionID = connID + sheetState.showPgRestoreSheet = true + } + } + } + + if dbType == .mysql { + menu.addSubmenu("Tasks", systemImage: "gearshape") { sub in + sub.addActionItem("Back Up", systemImage: "arrow.down.doc") { + sheetState.mysqlBackupDatabaseName = database.name + sheetState.mysqlBackupConnectionID = connID + sheetState.showMySQLBackupSheet = true + } + sub.addActionItem("Restore", systemImage: "arrow.up.doc") { + sheetState.mysqlBackupDatabaseName = database.name + sheetState.mysqlBackupConnectionID = connID + sheetState.showMySQLRestoreSheet = true + } + } + } + + if dbType == .sqlite && database.name.lowercased() != "main" && database.name.lowercased() != "temp" { + menu.addDivider() + menu.addActionItem("Detach Database", systemImage: "externaldrive.badge.minus") { + sheetState.detachDatabaseName = database.name + sheetState.detachConnectionID = connID + sheetState.showDetachSheet = true + } + } + + if dbType == .microsoftSQL { + menu.addSubmenu("Advanced Objects", systemImage: "puzzlepiece.extension") { sub in + sub.addActionItem("Change Tracking", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90") { + environmentState.openMSSQLAdvancedObjectsTab(connectionID: connID, databaseName: database.name, section: .changeTracking) + } + sub.addActionItem("Change Data Capture", systemImage: "arrow.triangle.branch") { + environmentState.openMSSQLAdvancedObjectsTab(connectionID: connID, databaseName: database.name, section: .cdc) + } + sub.addActionItem("Full-Text Search", systemImage: "text.magnifyingglass") { + environmentState.openMSSQLAdvancedObjectsTab(connectionID: connID, databaseName: database.name, section: .fullTextSearch) + } + sub.addActionItem("Replication", systemImage: "arrow.triangle.swap") { + environmentState.openMSSQLAdvancedObjectsTab(connectionID: connID, databaseName: database.name, section: .replication) + } + } + + menu.addSubmenu("Tasks", systemImage: "gearshape") { sub in + if database.isOnline { + sub.addActionItem("Back Up", systemImage: "arrow.down.doc") { + environmentState.openMaintenanceBackups(connectionID: connID, databaseName: database.name, action: .backup) + } + sub.addActionItem("Restore", systemImage: "arrow.up.doc") { + environmentState.openMaintenanceBackups(connectionID: connID, databaseName: database.name, action: .restore) + } + sub.addDivider() + sub.addActionItem("Shrink Database", systemImage: "arrow.down.right.and.arrow.up.left") { + Task { await self.runMSSQLTask(session: session, database: database.name, task: .shrink) } + } + sub.addDivider() + sub.addActionItem("Take Offline", systemImage: "bolt.slash") { + Task { await self.runMSSQLTask(session: session, database: database.name, task: .takeOffline) } + } + sub.addDivider() + sub.addActionItem("Detach Database", systemImage: "externaldrive.badge.minus") { + sheetState.detachDatabaseName = database.name + sheetState.detachConnectionID = connID + sheetState.showDetachSheet = true + } + sub.addDivider() + sub.addActionItem("Generate Scripts", systemImage: "scroll") { + sheetState.generateScriptsDatabaseName = database.name + sheetState.generateScriptsConnectionID = connID + sheetState.showGenerateScriptsWizard = true + } + sub.addActionItem("Import Flat File", systemImage: "square.and.arrow.down.on.square") { + sheetState.quickImportDatabaseName = database.name + sheetState.quickImportConnectionID = connID + sheetState.showQuickImportSheet = true + } + sub.addActionItem("Migrate Data", systemImage: "arrow.right.arrow.left") { + sheetState.dataMigrationConnectionID = connID + sheetState.showDataMigrationWizard = true + } + sub.addActionItem("Visual Query Builder", systemImage: "hammer") { + environmentState.openQueryBuilderTab(connectionID: connID) + } + sub.addDivider() + sub.addActionItem("Data-tier Application Tasks", systemImage: "archivebox") { + sheetState.dacWizardDatabaseName = database.name + sheetState.dacWizardConnectionID = connID + sheetState.showDACWizard = true + } + } else { + sub.addActionItem("Bring Online", systemImage: "bolt") { + Task { await self.runMSSQLTask(session: session, database: database.name, task: .bringOnline) } + } + sub.addActionItem("Restore", systemImage: "arrow.up.doc") { + environmentState.openMaintenanceBackups(connectionID: connID, databaseName: database.name, action: .restore) + } + } + } + } + + menu.addDivider() + if dbType == .postgresql { + menu.addSubmenu("Drop Database", systemImage: "trash") { sub in + sub.addActionItem("Drop", systemImage: "trash") { + sheetState.dropDatabaseTarget = .init(sessionID: session.id, connectionID: connID, databaseName: database.name, databaseType: .postgresql, variant: .standard) + sheetState.showDropDatabaseAlert = true + } + sub.addActionItem("Drop (Cascade)", systemImage: "trash") { + sheetState.dropDatabaseTarget = .init(sessionID: session.id, connectionID: connID, databaseName: database.name, databaseType: .postgresql, variant: .cascade) + sheetState.showDropDatabaseAlert = true + } + sub.addActionItem("Drop (Force)", systemImage: "trash") { + sheetState.dropDatabaseTarget = .init(sessionID: session.id, connectionID: connID, databaseName: database.name, databaseType: .postgresql, variant: .force) + sheetState.showDropDatabaseAlert = true + } + } + } else { + menu.addActionItem("Drop Database", systemImage: "trash") { + sheetState.dropDatabaseTarget = .init(sessionID: session.id, connectionID: connID, databaseName: database.name, databaseType: dbType, variant: .standard) + sheetState.showDropDatabaseAlert = true + } + } + + menu.addDivider() + menu.addActionItem("Properties", systemImage: "info.circle") { + let value = environmentState.prepareDatabaseEditorWindow( + connectionSessionID: session.connection.id, + databaseName: database.name, + databaseType: dbType + ) + openWindow(id: DatabaseEditorWindow.sceneID, value: value) + } + + return menu + } + + private func databaseFolderMenu( + kind: ExperimentalObjectBrowserDatabaseFolderKind, + databaseName: String, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + + switch kind { + case .security: + menu.addActionItem("Open Security Management", systemImage: "lock.shield") { + environmentState.openDatabaseSecurityTab(connectionID: session.connection.id, databaseName: databaseName) + } + case .databaseTriggers: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + if let database = session.databaseStructure?.databases.first(where: { $0.name == databaseName }) { + loadDatabaseDDLTriggers(database: database, session: session) + } + } + menu.addActionItem("New Database Trigger", systemImage: "bolt") { + sheetState.newDBDDLTriggerConnectionID = session.connection.id + sheetState.newDBDDLTriggerDatabaseName = databaseName + sheetState.showNewDBDDLTriggerSheet = true + } + case .serviceBroker: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + if let database = session.databaseStructure?.databases.first(where: { $0.name == databaseName }) { + loadServiceBrokerData(database: database, session: session) + } + } + case .externalResources: + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + if let database = session.databaseStructure?.databases.first(where: { $0.name == databaseName }) { + loadExternalResources(database: database, session: session) + } + } + } + + return menu + } + + private func databaseSubfolderMenu( + title: String, + databaseName: String, + session: ConnectionSession + ) -> NSMenu? { + let menu = NSMenu() + switch title { + case "Message Types": + menu.addActionItem("New Message Type", systemImage: "plus") { + sheetState.newMessageTypeConnectionID = session.connection.id + sheetState.newMessageTypeDatabaseName = databaseName + sheetState.showNewMessageTypeSheet = true + } + case "Contracts": + menu.addActionItem("New Contract", systemImage: "plus") { + sheetState.newContractConnectionID = session.connection.id + sheetState.newContractDatabaseName = databaseName + sheetState.showNewContractSheet = true + } + case "Queues": + menu.addActionItem("New Queue", systemImage: "plus") { + sheetState.newQueueConnectionID = session.connection.id + sheetState.newQueueDatabaseName = databaseName + sheetState.showNewQueueSheet = true + } + case "Services": + menu.addActionItem("New Service", systemImage: "plus") { + sheetState.newServiceConnectionID = session.connection.id + sheetState.newServiceDatabaseName = databaseName + sheetState.showNewServiceSheet = true + } + case "Routes": + menu.addActionItem("New Route", systemImage: "plus") { + sheetState.newRouteConnectionID = session.connection.id + sheetState.newRouteDatabaseName = databaseName + sheetState.showNewRouteSheet = true + } + case "External Data Sources": + menu.addActionItem("New External Data Source", systemImage: "plus") { + sheetState.newExternalDataSourceConnectionID = session.connection.id + sheetState.newExternalDataSourceDatabaseName = databaseName + sheetState.showNewExternalDataSourceSheet = true + } + case "External Tables": + menu.addActionItem("New External Table", systemImage: "plus") { + sheetState.newExternalTableConnectionID = session.connection.id + sheetState.newExternalTableDatabaseName = databaseName + sheetState.showNewExternalTableSheet = true + } + case "External File Formats": + menu.addActionItem("New External File Format", systemImage: "plus") { + sheetState.newExternalFileFormatConnectionID = session.connection.id + sheetState.newExternalFileFormatDatabaseName = databaseName + sheetState.showNewExternalFileFormatSheet = true + } + default: + return nil + } + return menu + } + + private func objectGroupMenu( + for type: SchemaObjectInfo.ObjectType, + databaseName: String, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + + menu.addActionItem("Refresh", systemImage: "arrow.clockwise") { + Task { + let handle = AppDirector.shared.activityEngine.begin("Refreshing \(type.pluralDisplayName)", connectionSessionID: session.id) + await environmentState.loadSchemaForDatabase(databaseName, connectionSession: session) + handle.succeed() + } + } + + if let title = experimentalObjectGroupCreationTitle(for: type) { + menu.addDivider() + let hasDesigner = VisualEditorResolver.hasVisualEditor(for: type, databaseType: session.connection.databaseType) + + if hasDesigner { + menu.addActionItem(title, systemImage: experimentalObjectGroupCreationIcon(for: type)) { + openNewObjectInDesigner(type: type, session: session) + } + menu.addActionItem(title + " (SQL)", systemImage: "scroll") { + let schemaName = session.connection.databaseType == .microsoftSQL ? "dbo" : "public" + let sql = experimentalObjectGroupCreationSQL( + for: title, + databaseType: session.connection.databaseType, + schemaName: schemaName + ) + environmentState.openQueryTab(for: session, presetQuery: sql, database: databaseName) + } + } else if type == .extension { + menu.addActionItem(title, systemImage: experimentalObjectGroupCreationIcon(for: type)) { + environmentState.openExtensionsManagerTab(connectionID: session.connection.id, databaseName: databaseName) + } + } else { + menu.addActionItem(title, systemImage: experimentalObjectGroupCreationIcon(for: type)) { + let schemaName = session.connection.databaseType == .microsoftSQL ? "dbo" : "public" + let sql = experimentalObjectGroupCreationSQL( + for: title, + databaseType: session.connection.databaseType, + schemaName: schemaName + ) + environmentState.openQueryTab(for: session, presetQuery: sql, database: databaseName) + } + } + } + + return menu + } + + private func objectMenu( + for object: SchemaObjectInfo, + databaseName: String, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + let databaseType = session.connection.databaseType + + menu.addActionItem("New Query", systemImage: "doc.text") { + environmentState.openQueryTab(for: session, database: databaseName) + } + + if object.type == .extension { + menu.addActionItem("New Extension", systemImage: "puzzlepiece.extension") { + environmentState.openExtensionsManagerTab(connectionID: session.connection.id, databaseName: databaseName) + } + } + + menu.addDivider() + + if object.type == .table || object.type == .view || object.type == .materializedView { + menu.addActionItem("Data", systemImage: "tablecells") { + let sql = previewQuery(for: object, databaseType: databaseType) + environmentState.openQueryTab(for: session, presetQuery: sql, database: databaseName) + } + } + + if object.type == .table || object.type == .extension { + menu.addActionItem("Structure", systemImage: object.type == .extension ? "puzzlepiece.fill" : "square.stack.3d.up") { + environmentState.openStructureTab(for: session, object: object, databaseName: databaseName) + } + } + + if object.type == .table { + menu.addActionItem("Diagram", systemImage: "rectangle.connected.to.line.below") { + environmentState.openDiagramTab(for: session, object: object, activeDatabaseName: databaseName) + } + } + + if [.view, .materializedView, .function, .procedure, .trigger, .sequence, .type].contains(object.type) { + menu.addActionItem("Definition", systemImage: "doc.text") { + openDefinition(for: object, databaseName: databaseName, session: session) + } + } + + if object.type == .function || object.type == .procedure { + menu.addActionItem("Execute", systemImage: "play.circle") { + let sql = executeStatement(for: object, databaseType: databaseType) + environmentState.openQueryTab(for: session, presetQuery: sql, database: databaseName) + } + } + + if VisualEditorResolver.hasVisualEditor(for: object.type, databaseType: databaseType) { + menu.addActionItem("Edit in Designer", systemImage: "rectangle.and.pencil.and.ellipsis") { + openObjectInDesigner(object, session: session) + } + } + + if object.type == .procedure || object.type == .function { + menu.addActionItem("Modify", systemImage: "pencil.and.outline") { + openAlterDefinition(for: object, databaseName: databaseName, session: session) + } + } + + let scriptActions = ScriptActionResolver.actions(for: object.type, databaseType: databaseType) + if !scriptActions.isEmpty { + menu.addDivider() + menu.addSubmenu("Script as", systemImage: "scroll") { submenu in + let readActions = scriptActions.filter(\.isReadGroup) + let createActions = scriptActions.filter(\.isCreateModifyGroup) + let writeActions = scriptActions.filter(\.isWriteGroup) + let executeActions = scriptActions.filter(\.isExecuteGroup) + let destroyActions = scriptActions.filter(\.isDestroyGroup) + + addScriptActions(readActions, to: submenu, object: object, databaseName: databaseName, session: session) + if !readActions.isEmpty && !createActions.isEmpty { submenu.addDivider() } + addScriptActions(createActions, to: submenu, object: object, databaseName: databaseName, session: session) + if !createActions.isEmpty && !writeActions.isEmpty { submenu.addDivider() } + addScriptActions(writeActions, to: submenu, object: object, databaseName: databaseName, session: session) + if !writeActions.isEmpty && !executeActions.isEmpty { submenu.addDivider() } + if writeActions.isEmpty && !createActions.isEmpty && !executeActions.isEmpty { submenu.addDivider() } + addScriptActions(executeActions, to: submenu, object: object, databaseName: databaseName, session: session) + let hasNonDestroy = !executeActions.isEmpty || !writeActions.isEmpty || !createActions.isEmpty || !readActions.isEmpty + if hasNonDestroy && !destroyActions.isEmpty { submenu.addDivider() } + addScriptActions(destroyActions, to: submenu, object: object, databaseName: databaseName, session: session) + } + } + + if databaseType == .microsoftSQL || object.type == .table || object.type == .view { + menu.addDivider() + menu.addSubmenu("Tasks", systemImage: "checklist") { submenu in + if databaseType == .microsoftSQL { + submenu.addActionItem("Generate Scripts", systemImage: "applescript") { + sheetState.generateScriptsDatabaseName = databaseName + sheetState.generateScriptsConnectionID = session.connection.id + sheetState.showGenerateScriptsWizard = true + } + } + if object.type == .table { + submenu.addActionItem("Import Data", systemImage: "square.and.arrow.down") { + sheetState.quickImportDatabaseName = databaseName + sheetState.quickImportConnectionID = session.connection.id + sheetState.showQuickImportSheet = true + } + } + if object.type == .table && databaseType == .microsoftSQL && object.isSystemVersioned != true && object.isHistoryTable != true { + submenu.addActionItem("Enable System Versioning", systemImage: "clock.badge.checkmark") { + sheetState.enableVersioningConnectionID = session.connection.id + sheetState.enableVersioningDatabaseName = databaseName + sheetState.enableVersioningSchemaName = object.schema + sheetState.enableVersioningTableName = object.name + sheetState.showEnableVersioningSheet = true + } + } + } + } + + menu.addDivider() + menu.addActionItem("Drop \(object.type.displayName)", systemImage: "trash") { + let sql = dropStatement(for: object, databaseType: databaseType, includeIfExists: false) + environmentState.openQueryTab(for: session, presetQuery: sql, database: databaseName) + } + + if object.type == .table { + menu.addDivider() + menu.addActionItem("Properties", systemImage: "info.circle") { + let value = environmentState.prepareTablePropertiesWindow( + connectionSessionID: session.connection.id, + schemaName: object.schema, + tableName: object.name, + databaseType: databaseType + ) + openWindow(id: TablePropertiesWindow.sceneID, value: value) + } + } else if VisualEditorResolver.hasVisualEditor(for: object.type, databaseType: databaseType) { + menu.addDivider() + menu.addActionItem("Properties", systemImage: "info.circle") { + openObjectInDesigner(object, session: session) + } + } + + return menu + } + + private func snapshotMenu(snapshot: SQLServerDatabaseSnapshot, session: ConnectionSession) -> NSMenu { + let menu = NSMenu() + menu.addActionItem("Revert to Snapshot", systemImage: "arrow.uturn.backward") { + revertSnapshot(snapshot, session: session) + } + menu.addDivider() + menu.addActionItem("Delete Snapshot", systemImage: "trash") { + deleteSnapshot(snapshot, session: session) + } + return menu + } + + private func linkedServerMenu( + server: ExperimentalObjectBrowserSidebarViewModel.LinkedServerItem, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + menu.addActionItem("Test Connection", systemImage: "bolt.horizontal") { + testLinkedServer(name: server.name, session: session) + } + menu.addDivider() + let dropItem = menu.addActionItem("Drop", systemImage: "trash") { + sheetState.dropLinkedServerTarget = .init( + connectionID: session.connection.id, + serverName: server.name + ) + sheetState.showDropLinkedServerAlert = true + } + dropItem.isEnabled = session.permissions?.canManageLinkedServers ?? true + return menu + } + + private func serverTriggerMenu( + trigger: ExperimentalObjectBrowserSidebarViewModel.ServerTriggerItem, + session: ConnectionSession + ) -> NSMenu { + let menu = NSMenu() + if trigger.isDisabled { + menu.addActionItem("Enable", systemImage: "checkmark.circle") { + setServerTrigger(trigger.name, enabled: true, session: session) + } + } else { + menu.addActionItem("Disable", systemImage: "pause.circle") { + setServerTrigger(trigger.name, enabled: false, session: session) + } + } + menu.addDivider() + menu.addActionItem("Script as CREATE", systemImage: "doc.text") { + scriptServerTrigger(name: trigger.name, session: session) + } + menu.addDivider() + menu.addActionItem("Drop", systemImage: "trash") { + dropServerTrigger(name: trigger.name, session: session) + } + return menu + } + + private func agentJobMenu(for session: ConnectionSession) -> NSMenu { + let menu = NSMenu() + menu.addActionItem("Open in Tab", systemImage: "list.bullet.rectangle") { + environmentState.openJobQueueTab(for: session) + } + menu.addActionItem("Open in New Window", systemImage: "rectangle.portrait.and.arrow.right") { + let sessionID = environmentState.prepareJobQueueWindow(for: session) + openWindow(id: JobQueueWindow.sceneID, value: sessionID) + } + return menu + } + + private func addScriptActions( + _ actions: [ScriptAction], + to menu: NSMenu, + object: SchemaObjectInfo, + databaseName: String, + session: ConnectionSession + ) { + for action in actions { + menu.addActionItem(action.title(for: session.connection.databaseType), systemImage: action.systemImage) { + performScriptAction(action, object: object, databaseName: databaseName, session: session) + } + } + } +} + +private extension ExperimentalObjectBrowserSidebarView { + func openNewObjectInDesigner(type: SchemaObjectInfo.ObjectType, session: ConnectionSession) { + let connID = session.connection.id + let schema = session.connection.databaseType == .microsoftSQL ? "dbo" : "public" + + switch type { + case .view: + let value = environmentState.prepareViewEditorWindow( + connectionSessionID: connID, + schemaName: schema, + existingView: nil, + isMaterialized: false + ) + openWindow(id: ViewEditorWindow.sceneID, value: value) + case .materializedView: + let value = environmentState.prepareViewEditorWindow( + connectionSessionID: connID, + schemaName: schema, + existingView: nil, + isMaterialized: true + ) + openWindow(id: ViewEditorWindow.sceneID, value: value) + case .function: + let value = environmentState.prepareFunctionEditorWindow( + connectionSessionID: connID, + schemaName: schema, + existingFunction: nil + ) + openWindow(id: FunctionEditorWindow.sceneID, value: value) + case .trigger: + let value = environmentState.prepareTriggerEditorWindow( + connectionSessionID: connID, + schemaName: schema, + tableName: "", + existingTrigger: nil + ) + openWindow(id: TriggerEditorWindow.sceneID, value: value) + case .sequence: + let value = environmentState.prepareSequenceEditorWindow( + connectionSessionID: connID, + schemaName: schema, + existingSequence: nil + ) + openWindow(id: SequenceEditorWindow.sceneID, value: value) + case .type: + let value = environmentState.prepareTypeEditorWindow( + connectionSessionID: connID, + schemaName: schema, + existingType: nil, + typeCategory: .composite + ) + openWindow(id: TypeEditorWindow.sceneID, value: value) + default: + break + } + } + + func performScriptAction( + _ action: ScriptAction, + object: SchemaObjectInfo, + databaseName: String, + session: ConnectionSession + ) { + let databaseType = session.connection.databaseType + let qualified = qualifiedName(for: object, databaseType: databaseType) + let sql: String + + switch action { + case .select: + sql = makeSelectStatement( + qualifiedName: qualified, + columnLines: "*", + databaseType: databaseType, + limit: nil + ) + case .selectLimited(let limit): + sql = makeSelectStatement( + qualifiedName: qualified, + columnLines: "*", + databaseType: databaseType, + limit: limit + ) + case .create: + openDefinition(for: object, databaseName: databaseName, session: session, replaceCreateWith: nil) + return + case .createOrReplace: + openDefinition(for: object, databaseName: databaseName, session: session, replaceCreateWith: "CREATE OR REPLACE") + return + case .alter: + openAlterDefinition(for: object, databaseName: databaseName, session: session) + return + case .alterTable: + sql = "ALTER TABLE \(qualified)\n ADD new_column_name data_type;" + case .insert: + sql = "INSERT INTO \(qualified) (column1, column2)\nVALUES (value1, value2);" + case .update: + sql = "UPDATE \(qualified)\nSET column1 = value1\nWHERE condition;" + case .delete: + sql = "DELETE FROM \(qualified)\nWHERE condition;" + case .execute: + sql = executeStatement(for: object, databaseType: databaseType) + case .drop: + sql = dropStatement(for: object, databaseType: databaseType, includeIfExists: false) + case .dropIfExists: + sql = dropStatement(for: object, databaseType: databaseType, includeIfExists: true) + } + + environmentState.openQueryTab(for: session, presetQuery: sql, database: databaseName) + } + + func openDefinition( + for object: SchemaObjectInfo, + databaseName: String, + session: ConnectionSession, + replaceCreateWith replacement: String? = nil + ) { + Task { + do { + var definition = try await session.session.getObjectDefinition( + objectName: object.name, + schemaName: object.schema, + objectType: object.type, + database: databaseName + ) + if let replacement, + let range = definition.range(of: "CREATE", options: .caseInsensitive) { + definition = definition.replacingCharacters(in: range, with: replacement) + } + environmentState.openQueryTab(for: session, presetQuery: definition, database: databaseName) + } catch { + environmentState.lastError = DatabaseError.from(error) + } + } + } + + func openAlterDefinition(for object: SchemaObjectInfo, databaseName: String, session: ConnectionSession) { + Task { + do { + var definition = try await session.session.getObjectDefinition( + objectName: object.name, + schemaName: object.schema, + objectType: object.type, + database: databaseName + ) + if let range = definition.range(of: "CREATE", options: .caseInsensitive) { + definition = definition.replacingCharacters(in: range, with: "ALTER") + } + environmentState.openQueryTab(for: session, presetQuery: definition, database: databaseName) + } catch { + environmentState.lastError = DatabaseError.from(error) + } + } + } + + func openObjectInDesigner(_ object: SchemaObjectInfo, session: ConnectionSession) { + switch object.type { + case .view: + let value = environmentState.prepareViewEditorWindow( + connectionSessionID: session.connection.id, + schemaName: object.schema, + existingView: object.name, + isMaterialized: false + ) + openWindow(id: ViewEditorWindow.sceneID, value: value) + case .materializedView: + let value = environmentState.prepareViewEditorWindow( + connectionSessionID: session.connection.id, + schemaName: object.schema, + existingView: object.name, + isMaterialized: true + ) + openWindow(id: ViewEditorWindow.sceneID, value: value) + case .trigger: + let value = environmentState.prepareTriggerEditorWindow( + connectionSessionID: session.connection.id, + schemaName: object.schema, + tableName: object.triggerTable ?? "", + existingTrigger: object.name + ) + openWindow(id: TriggerEditorWindow.sceneID, value: value) + case .function: + let value = environmentState.prepareFunctionEditorWindow( + connectionSessionID: session.connection.id, + schemaName: object.schema, + existingFunction: object.name + ) + openWindow(id: FunctionEditorWindow.sceneID, value: value) + case .sequence: + let value = environmentState.prepareSequenceEditorWindow( + connectionSessionID: session.connection.id, + schemaName: object.schema, + existingSequence: object.name + ) + openWindow(id: SequenceEditorWindow.sceneID, value: value) + case .type: + let value = environmentState.prepareTypeEditorWindow( + connectionSessionID: session.connection.id, + schemaName: object.schema, + existingType: object.name, + typeCategory: .composite + ) + openWindow(id: TypeEditorWindow.sceneID, value: value) + default: + break + } + } + + func previewQuery(for object: SchemaObjectInfo, databaseType: DatabaseType) -> String { + let qualified = qualifiedName(for: object, databaseType: databaseType) + return switch databaseType { + case .microsoftSQL: + "SELECT TOP 1000 * FROM \(qualified);" + default: + "SELECT * FROM \(qualified) LIMIT 1000;" + } + } + + func executeStatement(for object: SchemaObjectInfo, databaseType: DatabaseType) -> String { + let qualified = qualifiedName(for: object, databaseType: databaseType) + return switch databaseType { + case .microsoftSQL: + "EXEC \(qualified);" + case .postgresql: + "SELECT * FROM \(qualified)();" + case .mysql, .sqlite: + "CALL \(qualified)();" + } + } + + func dropStatement(for object: SchemaObjectInfo, databaseType: DatabaseType, includeIfExists: Bool) -> String { + let qualified = qualifiedName(for: object, databaseType: databaseType) + let keyword: String = switch object.type { + case .table: "TABLE" + case .view: "VIEW" + case .materializedView: "MATERIALIZED VIEW" + case .function: "FUNCTION" + case .procedure: "PROCEDURE" + case .trigger: "TRIGGER" + case .extension: "EXTENSION" + case .sequence: "SEQUENCE" + case .type: "TYPE" + case .synonym: "SYNONYM" + } + let ifExists = includeIfExists ? " IF EXISTS" : "" + return "DROP \(keyword)\(ifExists) \(qualified);" + } + + func qualifiedName(for object: SchemaObjectInfo, databaseType: DatabaseType) -> String { + switch databaseType { + case .microsoftSQL: + "[\(object.schema)].[\(object.name)]" + case .postgresql: + "\"\(object.schema.replacingOccurrences(of: "\"", with: "\"\""))\".\"\(object.name.replacingOccurrences(of: "\"", with: "\"\""))\"" + case .mysql: + "`\(object.schema)`.`\(object.name)`" + case .sqlite: + "\"\(object.name.replacingOccurrences(of: "\"", with: "\"\""))\"" + } + } +} + +private func experimentalObjectGroupCreationTitle(for type: SchemaObjectInfo.ObjectType) -> String? { + switch type { + case .table: "New Table" + case .view: "New View" + case .materializedView: "New Materialized View" + case .function: "New Function" + case .procedure: "New Procedure" + case .trigger: "New Trigger" + case .extension: "New Extension" + case .sequence: "New Sequence" + case .type: "New Type" + case .synonym: "New Synonym" + } +} + +private func experimentalObjectGroupCreationIcon(for type: SchemaObjectInfo.ObjectType) -> String { + switch type { + case .table: "tablecells" + case .view: "eye" + case .materializedView: "eye" + case .function: "function" + case .procedure: "gearshape" + case .trigger: "bolt" + case .extension: "puzzlepiece.extension" + case .sequence: "number" + case .type: "t.square" + case .synonym: "arrow.triangle.swap" + } +} + +private func experimentalObjectGroupCreationSQL( + for title: String, + databaseType: DatabaseType, + schemaName: String +) -> String { + switch (title, databaseType) { + case ("New Table", .microsoftSQL): + "CREATE TABLE [\(schemaName)].[NewTable] (\n [Id] INT IDENTITY(1,1) PRIMARY KEY,\n [Name] NVARCHAR(100) NOT NULL\n);\nGO" + case ("New Table", .postgresql): + "CREATE TABLE \(schemaName).new_table (\n id SERIAL PRIMARY KEY,\n name TEXT NOT NULL\n);" + case ("New Table", .mysql): + "CREATE TABLE new_table (\n id INT AUTO_INCREMENT PRIMARY KEY,\n name VARCHAR(100) NOT NULL\n);" + case ("New Table", .sqlite): + "CREATE TABLE new_table (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL\n);" + case ("New View", .microsoftSQL): + "CREATE VIEW [\(schemaName)].[NewView]\nAS\n SELECT * FROM [\(schemaName)].[TableName];\nGO" + case ("New View", .postgresql): + "CREATE VIEW \(schemaName).new_view AS\n SELECT * FROM \(schemaName).table_name;" + case ("New View", _): + "CREATE VIEW new_view AS\n SELECT * FROM table_name;" + case ("New Materialized View", _): + "CREATE MATERIALIZED VIEW \(schemaName).new_materialized_view AS\n SELECT * FROM \(schemaName).table_name;" + case ("New Function", .microsoftSQL): + "CREATE FUNCTION [\(schemaName)].[NewFunction]\n(\n @param1 INT\n)\nRETURNS INT\nAS\nBEGIN\n RETURN @param1;\nEND;\nGO" + case ("New Function", .postgresql): + "CREATE FUNCTION \(schemaName).new_function(param1 INTEGER)\nRETURNS INTEGER\nLANGUAGE plpgsql\nAS $$\nBEGIN\n RETURN param1;\nEND;\n$$;" + case ("New Function", _): + "CREATE FUNCTION new_function(param1 INT)\nRETURNS INT\nDETERMINISTIC\nBEGIN\n RETURN param1;\nEND;" + case ("New Procedure", .microsoftSQL): + "CREATE PROCEDURE [\(schemaName)].[NewProcedure]\n @param1 INT\nAS\nBEGIN\n SET NOCOUNT ON;\n SELECT @param1;\nEND;\nGO" + case ("New Procedure", _): + "CREATE PROCEDURE \(schemaName).new_procedure(param1 INTEGER)\nLANGUAGE plpgsql\nAS $$\nBEGIN\n -- procedure body\nEND;\n$$;" + case ("New Trigger", .microsoftSQL): + "CREATE TRIGGER [\(schemaName)].[NewTrigger]\nON [\(schemaName)].[TableName]\nAFTER INSERT\nAS\nBEGIN\n SET NOCOUNT ON;\n -- trigger body\nEND;\nGO" + case ("New Trigger", .postgresql): + "CREATE TRIGGER new_trigger\n AFTER INSERT ON \(schemaName).table_name\n FOR EACH ROW\n EXECUTE FUNCTION \(schemaName).trigger_function();" + case ("New Trigger", _): + "CREATE TRIGGER new_trigger\n AFTER INSERT ON table_name\n FOR EACH ROW\nBEGIN\n -- trigger body\nEND;" + case ("New Sequence", .microsoftSQL): + "CREATE SEQUENCE [\(schemaName)].[NewSequence]\n AS INT\n START WITH 1\n INCREMENT BY 1;\nGO" + case ("New Sequence", _): + "CREATE SEQUENCE \(schemaName).new_sequence\n START WITH 1\n INCREMENT BY 1;" + case ("New Type", .microsoftSQL): + "CREATE TYPE [\(schemaName)].[NewType] AS TABLE (\n [Id] INT,\n [Name] NVARCHAR(100)\n);\nGO" + case ("New Type", _): + "CREATE TYPE \(schemaName).new_type AS (\n field1 TEXT,\n field2 INTEGER\n);" + case ("New Synonym", .microsoftSQL): + "CREATE SYNONYM [\(schemaName)].[NewSynonym]\n FOR [\(schemaName)].[TargetObject];\nGO" + default: + "-- \(title)" + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+DatabaseSections.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+DatabaseSections.swift new file mode 100644 index 000000000..c9a7e00f8 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+DatabaseSections.swift @@ -0,0 +1,139 @@ +import SwiftUI +import SQLServerKit + +extension ExperimentalObjectBrowserSidebarView { + func loadDatabaseSecurityIfNeeded(database: DatabaseInfo, session: ConnectionSession) { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + let hasData = !(viewModel.dbSecurityUsersByDB[dbKey] ?? []).isEmpty + || !(viewModel.dbSecurityRolesByDB[dbKey] ?? []).isEmpty + || !(viewModel.dbSecurityAppRolesByDB[dbKey] ?? []).isEmpty + || !(viewModel.dbSecuritySchemasByDB[dbKey] ?? []).isEmpty + let isLoading = viewModel.dbSecurityLoadingByDB[dbKey] ?? false + if !hasData && !isLoading { + loadDatabaseSecurity(database: database, session: session) + } + } + + func loadDatabaseSecurity(database: DatabaseInfo, session: ConnectionSession) { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + viewModel.dbSecurityLoadingByDB[dbKey] = true + + Task { + defer { viewModel.dbSecurityLoadingByDB[dbKey] = false } + guard let mssql = session.session as? MSSQLSession else { return } + _ = try? await session.session.sessionForDatabase(database.name) + let security = mssql.security + + do { + let users = try await security.listUsers() + viewModel.dbSecurityUsersByDB[dbKey] = users + .filter { $0.name != "sys" && $0.name != "INFORMATION_SCHEMA" } + .map { .init(id: $0.name, name: $0.name, userType: String(describing: $0.type), defaultSchema: $0.defaultSchema) } + } catch { + viewModel.dbSecurityUsersByDB[dbKey] = [] + } + + do { + let roles = try await security.listRoles() + viewModel.dbSecurityRolesByDB[dbKey] = roles.map { + .init(id: $0.name, name: $0.name, isFixed: $0.isFixedRole, owner: $0.ownerPrincipalId.map(String.init)) + } + } catch { + viewModel.dbSecurityRolesByDB[dbKey] = [] + } + + viewModel.dbSecurityAppRolesByDB[dbKey] = [] + + do { + let schemas = try await security.listSchemas() + let systemSchemas: Set = [ + "sys", "INFORMATION_SCHEMA", "guest", + "db_owner", "db_accessadmin", "db_securityadmin", + "db_ddladmin", "db_backupoperator", "db_datareader", + "db_datawriter", "db_denydatareader", "db_denydatawriter" + ] + viewModel.dbSecuritySchemasByDB[dbKey] = schemas + .filter { !systemSchemas.contains($0.name) } + .map { .init(id: $0.name, name: $0.name, owner: $0.owner) } + } catch { + viewModel.dbSecuritySchemasByDB[dbKey] = [] + } + } + } + + func loadDatabaseDDLTriggers(database: DatabaseInfo, session: ConnectionSession) { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + guard let mssql = session.session as? MSSQLSession else { return } + viewModel.dbDDLTriggersLoadingByDB[dbKey] = true + + Task { + defer { viewModel.dbDDLTriggersLoadingByDB[dbKey] = false } + do { + let triggers = try await mssql.triggers.listDatabaseDDLTriggers(database: database.name) + viewModel.dbDDLTriggersByDB[dbKey] = triggers.map { + .init(id: $0.name, name: $0.name, isDisabled: $0.isDisabled, events: $0.events) + } + } catch { + viewModel.dbDDLTriggersByDB[dbKey] = [] + } + } + } + + func loadServiceBrokerData(database: DatabaseInfo, session: ConnectionSession) { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + guard let mssql = session.session as? MSSQLSession else { return } + viewModel.serviceBrokerLoadingByDB[dbKey] = true + + Task { + defer { viewModel.serviceBrokerLoadingByDB[dbKey] = false } + do { + let broker = mssql.serviceBroker + let dbName = database.name + let messageTypes = try await broker.listMessageTypes(database: dbName) + let contracts = try await broker.listContracts(database: dbName) + let queues = try await broker.listQueues(database: dbName) + let services = try await broker.listServices(database: dbName) + let routes = try await broker.listRoutes(database: dbName) + let bindings = try await broker.listRemoteServiceBindings(database: dbName) + + viewModel.serviceBrokerMessageTypesByDB[dbKey] = messageTypes.filter { !$0.isSystemObject }.map(\.name) + viewModel.serviceBrokerContractsByDB[dbKey] = contracts.filter { !$0.isSystemObject }.map(\.name) + viewModel.serviceBrokerQueuesByDB[dbKey] = queues.map { "\($0.schema).\($0.name)" } + viewModel.serviceBrokerServicesByDB[dbKey] = services.filter { !$0.isSystemObject }.map(\.name) + viewModel.serviceBrokerRoutesByDB[dbKey] = routes.map(\.name) + viewModel.serviceBrokerBindingsByDB[dbKey] = bindings.map(\.name) + } catch { + viewModel.serviceBrokerMessageTypesByDB[dbKey] = [] + viewModel.serviceBrokerContractsByDB[dbKey] = [] + viewModel.serviceBrokerQueuesByDB[dbKey] = [] + viewModel.serviceBrokerServicesByDB[dbKey] = [] + viewModel.serviceBrokerRoutesByDB[dbKey] = [] + viewModel.serviceBrokerBindingsByDB[dbKey] = [] + } + } + } + + func loadExternalResources(database: DatabaseInfo, session: ConnectionSession) { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + guard let mssql = session.session as? MSSQLSession else { return } + viewModel.externalResourcesLoadingByDB[dbKey] = true + + Task { + defer { viewModel.externalResourcesLoadingByDB[dbKey] = false } + do { + let polyBase = mssql.polyBase + let dbName = database.name + let sources = try await polyBase.listExternalDataSources(database: dbName) + let tables = try await polyBase.listExternalTables(database: dbName) + let formats = try await polyBase.listExternalFileFormats(database: dbName) + viewModel.externalDataSourcesByDB[dbKey] = sources.map(\.name) + viewModel.externalTablesByDB[dbKey] = tables.map { "\($0.schema).\($0.name)" } + viewModel.externalFileFormatsByDB[dbKey] = formats.map(\.name) + } catch { + viewModel.externalDataSourcesByDB[dbKey] = [] + viewModel.externalTablesByDB[dbKey] = [] + viewModel.externalFileFormatsByDB[dbKey] = [] + } + } + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Focus.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Focus.swift new file mode 100644 index 000000000..ea2163eb9 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Focus.swift @@ -0,0 +1,121 @@ +import EchoSense +import SwiftUI + +extension ExperimentalObjectBrowserSidebarView { + func handleExplorerFocus(_ focus: ExplorerFocus) { + Task { + await processExplorerFocus(focus) + } + } + + private func processExplorerFocus(_ focus: ExplorerFocus) async { + guard let session = await MainActor.run(body: { + environmentState.sessionGroup.sessionForConnection(focus.connectionID) + }) else { + await MainActor.run { + navigationStore.pendingExplorerFocus = nil + } + return + } + + await MainActor.run { + selectedConnectionID = focus.connectionID + environmentState.sessionGroup.setActiveSession(session.id) + viewModel.setExpanded(true, nodeID: ExperimentalObjectBrowserSidebarViewModel.serverNodeID(connectionID: focus.connectionID)) + viewModel.setExpanded(true, nodeID: ExperimentalObjectBrowserSidebarViewModel.databasesFolderNodeID(connectionID: focus.connectionID)) + viewModel.setExpanded(true, nodeID: ExperimentalObjectBrowserSidebarViewModel.databaseNodeID(connectionID: focus.connectionID, databaseName: focus.databaseName)) + let groupNodeID = ExperimentalObjectBrowserSidebarViewModel.objectGroupNodeID( + connectionID: focus.connectionID, + databaseName: focus.databaseName, + objectType: focus.objectType + ) + viewModel.setExpanded(true, nodeID: groupNodeID) + } + + let alreadyCached = await MainActor.run { + hasCachedSchema( + session: session, + databaseName: focus.databaseName, + schemaName: focus.schemaName, + objectName: focus.objectName, + objectType: focus.objectType + ) + } + + if !alreadyCached { + if session.sidebarFocusedDatabase?.localizedCaseInsensitiveCompare(focus.databaseName) != .orderedSame { + await environmentState.reconnectSession(session, to: focus.databaseName) + } + await environmentState.refreshDatabaseStructure( + for: session.id, + scope: .selectedDatabase, + databaseOverride: focus.databaseName + ) + } + + guard let refreshedSession = await MainActor.run(body: { + environmentState.sessionGroup.sessionForConnection(focus.connectionID) + }) else { + await MainActor.run { + navigationStore.pendingExplorerFocus = nil + } + return + } + + await MainActor.run { + applyExplorerFocus(focus, session: refreshedSession) + navigationStore.pendingExplorerFocus = nil + } + } + + private func hasCachedSchema( + session: ConnectionSession, + databaseName: String, + schemaName: String, + objectName: String, + objectType: SchemaObjectInfo.ObjectType + ) -> Bool { + let structure = session.databaseStructure + guard let structure, + let database = structure.databases.first(where: { + $0.name.localizedCaseInsensitiveCompare(databaseName) == .orderedSame + }), + let schema = database.schemas.first(where: { + $0.name.localizedCaseInsensitiveCompare(schemaName) == .orderedSame + }) else { + return false + } + + return schema.objects.contains(where: { + $0.type == objectType && $0.name.localizedCaseInsensitiveCompare(objectName) == .orderedSame + }) + } + + private func applyExplorerFocus(_ focus: ExplorerFocus, session: ConnectionSession) { + guard let structure = session.databaseStructure, + let database = structure.databases.first(where: { $0.name.localizedCaseInsensitiveCompare(focus.databaseName) == .orderedSame }), + let schema = database.schemas.first(where: { $0.name.localizedCaseInsensitiveCompare(focus.schemaName) == .orderedSame }), + let object = schema.objects.first(where: { $0.type == focus.objectType && $0.name.localizedCaseInsensitiveCompare(focus.objectName) == .orderedSame }) else { + return + } + + let objectGroupID = ExperimentalObjectBrowserSidebarViewModel.objectGroupNodeID( + connectionID: focus.connectionID, + databaseName: database.name, + objectType: object.type + ) + let objectNodeID = ExplorerSidebarIdentity.object( + connectionID: focus.connectionID, + databaseName: database.name, + objectID: object.id + ) + + session.sidebarFocusedDatabase = database.name + viewModel.selectedNodeID = objectNodeID + viewModel.setExpanded(true, nodeID: ExperimentalObjectBrowserSidebarViewModel.serverNodeID(connectionID: focus.connectionID)) + viewModel.setExpanded(true, nodeID: ExperimentalObjectBrowserSidebarViewModel.databasesFolderNodeID(connectionID: focus.connectionID)) + viewModel.setExpanded(true, nodeID: ExperimentalObjectBrowserSidebarViewModel.databaseNodeID(connectionID: focus.connectionID, databaseName: database.name)) + viewModel.setExpanded(true, nodeID: objectGroupID) + viewModel.revealAndPulse(nodeID: objectNodeID) + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Overlays.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Overlays.swift new file mode 100644 index 000000000..53ae92e72 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Overlays.swift @@ -0,0 +1,417 @@ +import SwiftUI +import SQLServerKit + +extension ExperimentalObjectBrowserSidebarView { + func applySheets(to content: V) -> some View { + content + .sheet(isPresented: $sheetState.showNewJobSheet) { + if let connID = sheetState.newJobSessionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + NewAgentJobSheet(session: session, environmentState: environmentState) { + sheetState.showNewJobSheet = false + loadAgentJobs(session: session) + } + } + } + .sheet(isPresented: $sheetState.showNewDatabaseSheet) { + if let connID = sheetState.newDatabaseConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + NewDatabaseSheet( + session: session, + environmentState: environmentState, + onDismiss: { sheetState.showNewDatabaseSheet = false } + ) + } + } + .sheet(isPresented: $sheetState.showNewServerRoleSheet) { + if let connID = sheetState.newSecuritySheetSessionID, + let session = environmentState.sessionGroup.activeSessions.first(where: { $0.id == connID }) { + NewServerRoleSheet(session: session) { + sheetState.showNewServerRoleSheet = false + loadServerSecurity(session: session) + } + } + } + .sheet(isPresented: $sheetState.showNewCredentialSheet) { + if let connID = sheetState.newSecuritySheetSessionID, + let session = environmentState.sessionGroup.activeSessions.first(where: { $0.id == connID }) { + NewCredentialSheet(session: session) { + sheetState.showNewCredentialSheet = false + loadServerSecurity(session: session) + } + } + } + .sheet(isPresented: $sheetState.showSecurityPGRoleSheet) { + if let connID = sheetState.securityPGRoleSheetSessionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + SecurityPGRoleSheet( + session: session, + environmentState: environmentState, + existingRoleName: sheetState.securityPGRoleSheetEditName + ) { + sheetState.showSecurityPGRoleSheet = false + loadServerSecurity(session: session) + } + } + } + .sheet(isPresented: $sheetState.showPgBackupSheet) { + if let dbName = sheetState.pgBackupDatabaseName, + let connID = sheetState.pgBackupConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + PgBackupSheetContainer( + connection: session.connection, + session: session.session, + databaseName: dbName, + isPresented: $sheetState.showPgBackupSheet + ) + } + } + .sheet(isPresented: $sheetState.showPgRestoreSheet) { + if let dbName = sheetState.pgBackupDatabaseName, + let connID = sheetState.pgBackupConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + PgRestoreSheetContainer( + connection: session.connection, + session: session.session, + databaseName: dbName, + connectionSession: session, + isPresented: $sheetState.showPgRestoreSheet + ) + } + } + .sheet(isPresented: $sheetState.showMySQLBackupSheet) { + if let dbName = sheetState.mysqlBackupDatabaseName, + let connID = sheetState.mysqlBackupConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + MySQLBackupSheetContainer( + connection: session.connection, + session: session.session, + databaseName: dbName, + isPresented: $sheetState.showMySQLBackupSheet + ) + } + } + .sheet(isPresented: $sheetState.showMySQLRestoreSheet) { + if let dbName = sheetState.mysqlBackupDatabaseName, + let connID = sheetState.mysqlBackupConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + MySQLRestoreSheetContainer( + connection: session.connection, + session: session.session, + databaseName: dbName, + connectionSession: session, + isPresented: $sheetState.showMySQLRestoreSheet + ) + } + } + .sheet(isPresented: $sheetState.showNewLinkedServerSheet) { + if let connID = sheetState.newLinkedServerSessionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + NewLinkedServerSheet( + session: session, + environmentState: environmentState + ) { + sheetState.showNewLinkedServerSheet = false + loadLinkedServers(session: session) + } + } + } + .sheet(isPresented: $sheetState.showCMSSheet) { + if let connID = sheetState.cmsConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + CMSSheet(session: session, onDismiss: { sheetState.showCMSSheet = false }) + } + } + .sheet(isPresented: $sheetState.showDetachSheet) { + if let dbName = sheetState.detachDatabaseName, + let connID = sheetState.detachConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + if session.connection.databaseType == .sqlite { + SQLiteDetachDatabaseSheet( + databaseName: dbName, + session: session, + environmentState: environmentState, + onDismiss: { sheetState.showDetachSheet = false } + ) + } else { + DetachDatabaseSheet( + databaseName: dbName, + session: session, + environmentState: environmentState, + onDismiss: { sheetState.showDetachSheet = false } + ) + } + } + } + .sheet(isPresented: $sheetState.showAttachSheet) { + if let connID = sheetState.attachConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + if session.connection.databaseType == .sqlite { + SQLiteAttachDatabaseSheet( + session: session, + environmentState: environmentState, + onDismiss: { sheetState.showAttachSheet = false } + ) + } else { + AttachDatabaseSheet( + session: session, + environmentState: environmentState, + onDismiss: { sheetState.showAttachSheet = false } + ) + } + } + } + .sheet(isPresented: $sheetState.showCreateSnapshotSheet) { + if let connID = sheetState.createSnapshotConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + CreateSnapshotSheet( + session: session, + environmentState: environmentState, + onDismiss: { + sheetState.showCreateSnapshotSheet = false + loadDatabaseSnapshots(session: session) + } + ) + } + } + .sheet(isPresented: $sheetState.showNewServerTriggerSheet) { + if let connID = sheetState.newServerTriggerConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + NewServerTriggerSheet(session: session, environmentState: environmentState) { + sheetState.showNewServerTriggerSheet = false + loadServerTriggers(session: session) + } + } + } + .sheet(isPresented: $sheetState.showNewDBDDLTriggerSheet) { + if let connID = sheetState.newDBDDLTriggerConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newDBDDLTriggerDatabaseName { + NewDatabaseDDLTriggerSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewDBDDLTriggerSheet = false + } + } + } + .sheet(isPresented: $sheetState.showNewMessageTypeSheet) { + if let connID = sheetState.newMessageTypeConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newMessageTypeDatabaseName { + NewMessageTypeSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewMessageTypeSheet = false + } + } + } + .sheet(isPresented: $sheetState.showNewContractSheet) { + if let connID = sheetState.newContractConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newContractDatabaseName { + NewContractSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewContractSheet = false + } + } + } + .sheet(isPresented: $sheetState.showNewQueueSheet) { + if let connID = sheetState.newQueueConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newQueueDatabaseName { + NewQueueSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewQueueSheet = false + } + } + } + .sheet(isPresented: $sheetState.showNewServiceSheet) { + if let connID = sheetState.newServiceConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newServiceDatabaseName { + NewServiceSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewServiceSheet = false + } + } + } + .sheet(isPresented: $sheetState.showNewRouteSheet) { + if let connID = sheetState.newRouteConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newRouteDatabaseName { + NewRouteSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewRouteSheet = false + } + } + } + .sheet(isPresented: $sheetState.showNewExternalDataSourceSheet) { + if let connID = sheetState.newExternalDataSourceConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newExternalDataSourceDatabaseName { + NewExternalDataSourceSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewExternalDataSourceSheet = false + } + } + } + .sheet(isPresented: $sheetState.showNewExternalFileFormatSheet) { + if let connID = sheetState.newExternalFileFormatConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newExternalFileFormatDatabaseName { + NewExternalFileFormatSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewExternalFileFormatSheet = false + } + } + } + .sheet(isPresented: $sheetState.showNewExternalTableSheet) { + if let connID = sheetState.newExternalTableConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.newExternalTableDatabaseName { + NewExternalTableSheet(databaseName: dbName, session: session, environmentState: environmentState) { + sheetState.showNewExternalTableSheet = false + } + } + } + .sheet(isPresented: $sheetState.showGenerateScriptsWizard) { + if let connID = sheetState.generateScriptsConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let dbName = sheetState.generateScriptsDatabaseName { + let viewModel = GenerateScriptsWizardViewModel( + session: session.session, + databaseName: dbName, + databaseType: session.connection.databaseType + ) + GenerateScriptsWizardView(viewModel: viewModel) + .onAppear { + viewModel.onOpenInQueryTab = { script in + environmentState.openQueryTab(for: session, presetQuery: script, database: dbName) + } + } + } + } + .sheet(isPresented: $sheetState.showQuickImportSheet) { + if let connID = sheetState.quickImportConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + QuickImportSheet(viewModel: QuickImportViewModel(session: session.session)) + } + } + .sheet(isPresented: $sheetState.showDACWizard) { + if let connID = sheetState.dacWizardConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID) { + DACWizardView( + viewModel: DACWizardViewModel( + session: session.session, + databaseName: sheetState.dacWizardDatabaseName ?? "" + ) + ) + } + } + .sheet(isPresented: $sheetState.showEnableVersioningSheet) { + if let connID = sheetState.enableVersioningConnectionID, + let session = environmentState.sessionGroup.sessionForConnection(connID), + let schema = sheetState.enableVersioningSchemaName, + let table = sheetState.enableVersioningTableName { + EnableSystemVersioningSheet( + tableName: table, + schemaName: schema, + session: session, + environmentState: environmentState + ) { + sheetState.showEnableVersioningSheet = false + } + } + } + .sheet(isPresented: $sheetState.showDataMigrationWizard) { + let viewModel = DataMigrationWizardViewModel() + let sessions = environmentState.sessionGroup.activeSessions + DataMigrationWizardView(viewModel: viewModel) + .onAppear { + viewModel.availableSessions = sessions + if let connID = sheetState.dataMigrationConnectionID, + let session = sessions.first(where: { $0.connection.id == connID }) { + viewModel.sourceSessionID = session.id + viewModel.loadSourceDatabases() + } + viewModel.onOpenInQueryTab = { [weak environmentState] script in + let targetSession = sessions.first(where: { $0.id == viewModel.targetSessionID }) + environmentState?.openQueryTab( + for: targetSession, + presetQuery: script, + database: viewModel.targetDatabaseName + ) + } + } + } + } + + func applyAlerts(to content: V) -> some View { + content + .alert( + "Drop \"\(sheetState.dropDatabaseTarget?.databaseName ?? "")\"?", + isPresented: $sheetState.showDropDatabaseAlert + ) { + Button("Cancel", role: .cancel) { + sheetState.dropDatabaseTarget = nil + } + Button("Drop", role: .destructive) { + guard let target = sheetState.dropDatabaseTarget else { return } + sheetState.dropDatabaseTarget = nil + guard let session = environmentState.sessionGroup.sessionForConnection(target.connectionID) else { return } + Task { + switch target.databaseType { + case .postgresql: + await dropPostgresDatabase( + session: session, + name: target.databaseName, + cascade: target.variant == .cascade, + force: target.variant == .force + ) + default: + await runMSSQLTask(session: session, database: target.databaseName, task: .drop) + } + } + } + } message: { + if let target = sheetState.dropDatabaseTarget { + switch target.variant { + case .cascade: + Text("This will drop the database and all dependent objects. This action cannot be undone.") + case .force: + Text("This will forcefully terminate all connections and drop the database. This action cannot be undone.") + case .standard: + Text("This will permanently delete the database \"\(target.databaseName)\". This action cannot be undone.") + } + } + } + .alert( + "Delete linked server \"\(sheetState.dropLinkedServerTarget?.serverName ?? "")\"?", + isPresented: $sheetState.showDropLinkedServerAlert + ) { + Button("Cancel", role: .cancel) { + sheetState.dropLinkedServerTarget = nil + } + Button("Delete", role: .destructive) { + guard let target = sheetState.dropLinkedServerTarget else { return } + sheetState.dropLinkedServerTarget = nil + guard let session = environmentState.sessionGroup.sessionForConnection(target.connectionID) else { return } + Task { + await executeDropLinkedServer(target, session: session) + } + } + } message: { + Text("This will permanently remove the linked server and all its login mappings. This action cannot be undone.") + } + .alert( + "Drop \(sheetState.dropSecurityPrincipalTarget?.kind.rawValue ?? "") \"\(sheetState.dropSecurityPrincipalTarget?.name ?? "")\"?", + isPresented: $sheetState.showDropSecurityPrincipalAlert + ) { + Button("Cancel", role: .cancel) { + sheetState.dropSecurityPrincipalTarget = nil + } + Button("Drop", role: .destructive) { + guard let target = sheetState.dropSecurityPrincipalTarget else { return } + sheetState.dropSecurityPrincipalTarget = nil + guard let session = environmentState.sessionGroup.sessionForConnection(target.connectionID) else { return } + Task { + await executeDropSecurityPrincipal(target, session: session) + } + } + } message: { + if let target = sheetState.dropSecurityPrincipalTarget { + Text("This will permanently drop the \(target.kind.rawValue.lowercased()) \"\(target.name)\". This action cannot be undone.") + } + } + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Security.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Security.swift new file mode 100644 index 000000000..8bf8a9877 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+Security.swift @@ -0,0 +1,203 @@ +import SwiftUI +import PostgresKit +import SQLServerKit + +extension ExperimentalObjectBrowserSidebarView { + func loadServerSecurityIfNeeded(session: ConnectionSession) { + let connID = session.connection.id + let hasData = !(viewModel.securityLoginsBySession[connID] ?? []).isEmpty + || !(viewModel.securityServerRolesBySession[connID] ?? []).isEmpty + || !(viewModel.securityCredentialsBySession[connID] ?? []).isEmpty + let isLoading = viewModel.securityServerLoadingBySession[connID] ?? false + if !hasData && !isLoading { + loadServerSecurity(session: session) + } + } + + func loadServerSecurity(session: ConnectionSession) { + Task { + await loadServerSecurityAsync(session: session) + } + } + + func loadServerSecurityAsync(session: ConnectionSession) async { + let connID = session.connection.id + viewModel.securityServerLoadingBySession[connID] = true + defer { viewModel.securityServerLoadingBySession[connID] = false } + + switch session.connection.databaseType { + case .microsoftSQL: + await loadMSSQLServerSecurity(session: session, connID: connID) + case .postgresql: + await loadPostgresServerSecurity(session: session, connID: connID) + case .mysql, .sqlite: + break + } + } + + func loadMSSQLServerSecurity(session: ConnectionSession, connID: UUID) async { + guard let mssql = session.session as? MSSQLSession else { return } + let security = mssql.serverSecurity + + do { + let logins = try await security.listLogins(includeSystemLogins: false) + viewModel.securityLoginsBySession[connID] = logins.map { + .init( + id: $0.name, + name: $0.name, + loginType: loginTypeDisplayName($0.type), + isDisabled: $0.isDisabled + ) + } + } catch { + viewModel.securityLoginsBySession[connID] = [] + } + + do { + let roles = try await security.listServerRoles() + viewModel.securityServerRolesBySession[connID] = roles.map { + .init(id: $0.name, name: $0.name, isFixed: $0.isFixed) + } + } catch { + viewModel.securityServerRolesBySession[connID] = [] + } + + do { + let credentials = try await security.listCredentials() + viewModel.securityCredentialsBySession[connID] = credentials.map { + .init(id: $0.name, name: $0.name, identity: $0.identity ?? "") + } + } catch { + viewModel.securityCredentialsBySession[connID] = [] + } + } + + func loadPostgresServerSecurity(session: ConnectionSession, connID: UUID) async { + guard let pg = session.session as? PostgresSession else { return } + + do { + let roles = try await pg.client.security.listRoles() + viewModel.securityLoginsBySession[connID] = roles.map { role in + let typeDescription: String + if role.isSuperuser { + typeDescription = "Superuser" + } else if role.canLogin { + typeDescription = "Login Role" + } else { + typeDescription = "Group Role" + } + return .init( + id: role.name, + name: role.name, + loginType: typeDescription, + isDisabled: false + ) + } + } catch { + viewModel.securityLoginsBySession[connID] = [] + } + } + + func createMSSQLServerRole(session: ConnectionSession) { + sheetState.newSecuritySheetSessionID = session.id + sheetState.showNewServerRoleSheet = true + } + + func createMSSQLCredential(session: ConnectionSession) { + sheetState.newSecuritySheetSessionID = session.id + sheetState.showNewCredentialSheet = true + } + + func dropMSSQLLogin(name: String, session: ConnectionSession) async { + guard let mssql = session.session as? MSSQLSession else { return } + do { + try await mssql.serverSecurity.dropLogin(name: name) + loadServerSecurity(session: session) + environmentState.notificationEngine?.post(category: .securityDropped, message: "Login '\(name)' dropped") + } catch { + environmentState.notificationEngine?.post(category: .generalError, message: "Drop failed: \(readableErrorMessage(error))") + } + } + + func dropMSSQLServerRole(name: String, session: ConnectionSession) async { + guard let mssql = session.session as? MSSQLSession else { return } + do { + try await mssql.serverSecurity.dropServerRole(name: name) + loadServerSecurity(session: session) + environmentState.notificationEngine?.post(category: .securityDropped, message: "Server role '\(name)' dropped") + } catch { + environmentState.notificationEngine?.post(category: .generalError, message: "Drop failed: \(readableErrorMessage(error))") + } + } + + func enableMSSQLLogin(name: String, enabled: Bool, session: ConnectionSession) async { + guard let mssql = session.session as? MSSQLSession else { return } + do { + try await mssql.serverSecurity.enableLogin(name: name, enabled: enabled) + loadServerSecurity(session: session) + } catch { + environmentState.notificationEngine?.post( + category: .securityToggleFailed, + message: "Failed to \(enabled ? "enable" : "disable") login: \(readableErrorMessage(error))" + ) + } + } + + func dropPGRole(name: String, session: ConnectionSession) async { + guard let pg = session.session as? PostgresSession else { return } + do { + try await pg.client.security.dropUser(name: name) + loadServerSecurity(session: session) + environmentState.notificationEngine?.post(category: .securityDropped, message: "Role '\(name)' dropped") + } catch { + environmentState.notificationEngine?.post(category: .generalError, message: "Drop failed: \(readableErrorMessage(error))") + } + } + + func reassignPGRole(name: String, session: ConnectionSession) async { + let sql = """ + -- Reassign all objects owned by "\(name)" to another role. + -- Replace "target_role" with the role to receive the objects. + REASSIGN OWNED BY "\(name)" TO "target_role"; + """ + openScriptTab(sql: sql, session: session) + } + + func executeDropSecurityPrincipal( + _ target: SidebarSheetState.DropSecurityPrincipalTarget, + session: ConnectionSession + ) async { + switch target.kind { + case .pgRole: + await dropPGRole(name: target.name, session: session) + case .mssqlLogin: + await dropMSSQLLogin(name: target.name, session: session) + case .mssqlServerRole: + await dropMSSQLServerRole(name: target.name, session: session) + case .mssqlUser: + break + } + } + + func openScriptTab(sql: String, session: ConnectionSession) { + environmentState.openQueryTab(for: session, presetQuery: sql) + } + + func readableErrorMessage(_ error: Error) -> String { + if let pgError = error as? PostgresKit.PostgresError { + return pgError.message + } + return error.localizedDescription + } + + func loginTypeDisplayName(_ type: ServerLoginType) -> String { + switch type { + case .sql: "SQL" + case .windowsUser: "Windows" + case .windowsGroup: "Windows Group" + case .certificate: "Certificate" + case .asymmetricKey: "Asymmetric Key" + case .external: "External" + } + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+ServerData.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+ServerData.swift new file mode 100644 index 000000000..e763ee22e --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView+ServerData.swift @@ -0,0 +1,312 @@ +import SwiftUI +import PostgresKit +import SQLServerKit + +extension ExperimentalObjectBrowserSidebarView { + enum ExperimentalMSSQLDatabaseTask { + case shrink + case takeOffline + case bringOnline + case drop + } + + func loadAgentJobs(session: ConnectionSession) { + let connID = session.connection.id + guard let mssql = session.session as? MSSQLSession else { return } + viewModel.agentJobsLoadingBySession[connID] = true + + Task { + defer { viewModel.agentJobsLoadingBySession[connID] = false } + + do { + let detailed = try await mssql.agent.listJobDetails() + viewModel.agentJobsBySession[connID] = detailed.map { job in + .init( + id: job.jobId, + name: job.name, + enabled: job.enabled, + lastOutcome: job.lastRunOutcome + ) + } + } catch { + do { + let basic = try await mssql.agent.listJobs() + viewModel.agentJobsBySession[connID] = basic.map { job in + .init( + id: job.name, + name: job.name, + enabled: job.enabled, + lastOutcome: job.lastRunOutcome + ) + } + } catch { + viewModel.agentJobsBySession[connID] = [] + } + } + } + } + + func loadLinkedServers(session: ConnectionSession) { + let connID = session.connection.id + guard let mssql = session.session as? MSSQLSession else { return } + viewModel.linkedServersLoadingBySession[connID] = true + + Task { + defer { viewModel.linkedServersLoadingBySession[connID] = false } + + do { + let servers = try await mssql.linkedServers.list() + viewModel.linkedServersBySession[connID] = servers.map { server in + .init( + id: server.name, + name: server.name, + provider: server.provider, + dataSource: server.dataSource, + product: server.product, + isDataAccessEnabled: server.isDataAccessEnabled + ) + } + } catch { + viewModel.linkedServersBySession[connID] = [] + } + } + } + + func testLinkedServer(name: String, session: ConnectionSession) { + guard let mssql = session.session as? MSSQLSession else { return } + + Task { + do { + let success = try await mssql.linkedServers.test(name: name) + environmentState.toastPresenter.show( + icon: success ? "checkmark.circle" : "xmark.circle", + message: success ? "Connection to \"\(name)\" succeeded." : "Connection to \"\(name)\" failed.", + style: success ? .success : .error + ) + } catch { + environmentState.toastPresenter.show( + icon: "xmark.circle", + message: "Connection test failed: \(error.localizedDescription)", + style: .error + ) + } + } + } + + func executeDropLinkedServer(_ target: SidebarSheetState.DropLinkedServerTarget, session: ConnectionSession) async { + guard let mssql = session.session as? MSSQLSession else { return } + do { + try await mssql.linkedServers.drop(name: target.serverName, dropLogins: true) + loadLinkedServers(session: session) + } catch { + environmentState.toastPresenter.show( + icon: "xmark.circle", + message: "Failed to drop linked server: \(error.localizedDescription)", + style: .error + ) + } + } + + func loadSSISFoldersAsync(session: ConnectionSession) async { + let connID = session.connection.id + guard let mssql = session.session as? MSSQLSession else { return } + + viewModel.ssisLoadingBySession[connID] = true + defer { viewModel.ssisLoadingBySession[connID] = false } + + do { + if try await mssql.ssis.isSSISCatalogAvailable() { + viewModel.ssisFoldersBySession[connID] = try await mssql.ssis.listFolders() + } else { + viewModel.ssisFoldersBySession[connID] = [] + } + } catch { + viewModel.ssisFoldersBySession[connID] = [] + } + } + + func loadDatabaseSnapshots(session: ConnectionSession) { + let connID = session.connection.id + viewModel.databaseSnapshotsLoadingBySession[connID] = true + + Task { + defer { viewModel.databaseSnapshotsLoadingBySession[connID] = false } + + do { + viewModel.databaseSnapshotsBySession[connID] = try await session.session.listDatabaseSnapshots() + } catch { + viewModel.databaseSnapshotsBySession[connID] = [] + } + } + } + + func revertSnapshot(_ snapshot: SQLServerDatabaseSnapshot, session: ConnectionSession) { + Task { + let handle = AppDirector.shared.activityEngine.begin( + "Revert \(snapshot.sourceDatabaseName) to snapshot \(snapshot.name)", + connectionSessionID: session.id + ) + do { + try await session.session.revertToSnapshot(snapshotName: snapshot.name) + handle.succeed() + environmentState.notificationEngine?.post( + category: .maintenanceCompleted, + message: "Reverted \(snapshot.sourceDatabaseName) to snapshot \(snapshot.name)." + ) + } catch { + handle.fail(error.localizedDescription) + environmentState.notificationEngine?.post( + category: .maintenanceFailed, + message: "Revert failed: \(error.localizedDescription)" + ) + } + } + } + + func deleteSnapshot(_ snapshot: SQLServerDatabaseSnapshot, session: ConnectionSession) { + Task { + let handle = AppDirector.shared.activityEngine.begin( + "Delete snapshot \(snapshot.name)", + connectionSessionID: session.id + ) + do { + try await session.session.deleteDatabaseSnapshot(name: snapshot.name) + handle.succeed() + environmentState.notificationEngine?.post( + category: .maintenanceCompleted, + message: "Snapshot \(snapshot.name) deleted." + ) + loadDatabaseSnapshots(session: session) + } catch { + handle.fail(error.localizedDescription) + environmentState.notificationEngine?.post( + category: .maintenanceFailed, + message: "Delete snapshot failed: \(error.localizedDescription)" + ) + } + } + } + + func loadServerTriggers(session: ConnectionSession) { + let connID = session.connection.id + guard let mssql = session.session as? MSSQLSession else { return } + viewModel.serverTriggersLoadingBySession[connID] = true + + Task { + defer { viewModel.serverTriggersLoadingBySession[connID] = false } + + do { + let triggers = try await mssql.triggers.listServerTriggers() + viewModel.serverTriggersBySession[connID] = triggers.map { trigger in + .init( + id: trigger.name, + name: trigger.name, + isDisabled: trigger.isDisabled, + typeDescription: trigger.typeDescription, + events: trigger.events + ) + } + } catch { + viewModel.serverTriggersBySession[connID] = [] + } + } + } + + func setServerTrigger(_ name: String, enabled: Bool, session: ConnectionSession) { + guard let mssql = session.session as? MSSQLSession else { return } + Task { + do { + if enabled { + try await mssql.triggers.enableServerTrigger(name: name) + } else { + try await mssql.triggers.disableServerTrigger(name: name) + } + loadServerTriggers(session: session) + } catch { + environmentState.toastPresenter.show( + icon: "xmark.circle", + message: "Failed to \(enabled ? "enable" : "disable") trigger: \(error.localizedDescription)", + style: .error + ) + } + } + } + + func dropServerTrigger(name: String, session: ConnectionSession) { + guard let mssql = session.session as? MSSQLSession else { return } + Task { + do { + try await mssql.triggers.dropServerTrigger(name: name) + loadServerTriggers(session: session) + } catch { + environmentState.toastPresenter.show( + icon: "xmark.circle", + message: "Failed to drop trigger: \(error.localizedDescription)", + style: .error + ) + } + } + } + + func scriptServerTrigger(name: String, session: ConnectionSession) { + guard let mssql = session.session as? MSSQLSession else { return } + Task { + do { + if let definition = try await mssql.triggers.getServerTriggerDefinition(name: name) { + environmentState.openQueryTab(for: session, presetQuery: definition) + } + } catch { + environmentState.toastPresenter.show( + icon: "xmark.circle", + message: "Failed to get trigger definition: \(error.localizedDescription)", + style: .error + ) + } + } + } + + func dropPostgresDatabase(session: ConnectionSession, name: String, cascade: Bool, force: Bool) async { + guard let pgSession = session.session as? PostgresSession else { return } + + do { + _ = try await pgSession.client.admin.dropDatabase(name: name, ifExists: true, withForce: force) + await environmentState.refreshDatabaseStructure(for: session.id) + } catch { + environmentState.notificationEngine?.post( + category: .generalError, + message: "Drop failed: \(error.localizedDescription)" + ) + } + } + + func runMSSQLTask(session: ConnectionSession, database: String, task: ExperimentalMSSQLDatabaseTask) async { + guard let mssqlSession = session.session as? MSSQLSession else { return } + let admin = mssqlSession.admin + + do { + let messages: [SQLServerStreamMessage] + switch task { + case .shrink: + messages = try await admin.shrinkDatabase(name: database) + case .takeOffline: + messages = try await admin.takeDatabaseOffline(name: database) + case .bringOnline: + messages = try await admin.bringDatabaseOnline(name: database) + case .drop: + messages = try await admin.dropDatabase(name: database) + } + + let infoMessages = messages.filter { $0.kind == .info } + let toastMessage = infoMessages.map(\.message).joined(separator: " ").trimmingCharacters(in: .whitespacesAndNewlines) + if !toastMessage.isEmpty { + environmentState.notificationEngine?.post(category: .maintenanceCompleted, message: toastMessage) + } + await environmentState.refreshDatabaseStructure(for: session.id) + } catch { + environmentState.notificationEngine?.post( + category: .maintenanceFailed, + message: "Task failed: \(error.localizedDescription)" + ) + } + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView.swift new file mode 100644 index 000000000..178e005fe --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarView.swift @@ -0,0 +1,403 @@ +import SwiftUI + +struct ExperimentalObjectBrowserSidebarView: View { + @Binding var selectedConnectionID: UUID? + + @Environment(ProjectStore.self) var projectStore + @Environment(EnvironmentState.self) var environmentState + @Environment(NavigationStore.self) var navigationStore + @Environment(\.openWindow) var openWindow + + @State var viewModel = ExperimentalObjectBrowserSidebarViewModel() + @State var sheetState = SidebarSheetState() + + private var sessions: [ConnectionSession] { + environmentState.sessionGroup.sessions + } + + private var pendingConnections: [PendingConnection] { + environmentState.pendingConnections + } + + var body: some View { + let roots = ExperimentalObjectBrowserSnapshotBuilder.buildRoots( + pendingConnections: pendingConnections, + sessions: sessions, + settings: projectStore.globalSettings, + viewModel: viewModel + ) + + let mainContent = Group { + if sessions.isEmpty && pendingConnections.isEmpty { + VStack(spacing: SpacingTokens.xs) { + Image(systemName: "server.rack") + .font(TypographyTokens.hero.weight(.medium)) + .foregroundStyle(ColorTokens.Text.tertiary) + Text("No Connection") + .font(TypographyTokens.standard) + .foregroundStyle(ColorTokens.Text.secondary) + } + .padding(.vertical, SpacingTokens.xl2) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } else { + ExperimentalObjectBrowserOutlineView( + roots: roots, + expandedNodeIDs: viewModel.expandedNodeIDs, + selectedNodeID: viewModel.selectedNodeID, + rowContent: { node, isExpanded, outlineLevel, outlineOffset, onActivate in + AnyView( + ExperimentalObjectBrowserRowView( + node: node, + isExpanded: isExpanded, + isSelected: viewModel.selectedNodeID == node.id, + outlineLevel: outlineLevel, + outlineOffset: outlineOffset, + isHighlighted: viewModel.highlightedNodeID == node.id, + highlightPulse: viewModel.highlightPulse, + contextMenuBuilder: { contextMenu(for: node) }, + onActivate: onActivate + ) + .environment(projectStore) + .environment(environmentState) + .environment(\.sidebarDensity, projectStore.globalSettings.sidebarDensity) + ) + }, + onExpansionChanged: { node, isExpanded in + handleExpansionChange(of: node, isExpanded: isExpanded) + }, + onActivation: { node in + handleActivation(of: node) + }, + onSelectionChanged: { node in + handleSelectionChange(node) + }, + revealNodeID: viewModel.revealedNodeID, + revealRequestID: viewModel.revealRequestID + ) + .background(Color.clear) + } + } + .environment(sheetState) + .environment(\.sidebarDensity, projectStore.globalSettings.sidebarDensity) + .task(id: projectStore.selectedProject?.id) { + restoreAndSynchronizeState() + } + .onAppear { + schedulePendingNavigationConsumption() + } + .onChange(of: sessions.map(\.connection.id)) { _, _ in + synchronizeDefaults() + } + .onChange(of: viewModel.expandedNodeIDs) { _, _ in + guard !sessions.isEmpty else { return } + viewModel.persistExpansionState(projectID: projectStore.selectedProject?.id) + } + .onChange(of: sessions.map(\.id)) { oldIDs, newIDs in + let added = Set(newIDs).subtracting(oldIDs) + guard let newSession = sessions.first(where: { added.contains($0.id) }) else { return } + focusNewSession(newSession) + } + .onChange(of: navigationStore.pendingExplorerFocus) { _, focus in + guard let focus else { return } + handleExplorerFocus(focus) + } + .onChange(of: navigationStore.pendingExplorerRevealRequestID) { _, _ in + guard let connectionID = navigationStore.pendingExplorerRevealConnectionID else { return } + revealConnection(connectionID) + } + + let withSheets = applySheets(to: mainContent) + let withAlerts = applyAlerts(to: withSheets) + withAlerts + } + + private func synchronizeDefaults() { + viewModel.synchronizeDefaults(sessions: sessions) { databaseType in + projectStore.globalSettings.sidebarExpandSections(for: databaseType) + } + if !sessions.isEmpty { + viewModel.persistExpansionState(projectID: projectStore.selectedProject?.id) + } + + for session in sessions { + let securityNodeID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: session.connection.id, + kind: .security + ) + if viewModel.isExpanded(securityNodeID) { + loadServerSecurityIfNeeded(session: session) + } + } + + if selectedConnectionID == nil { + selectedConnectionID = sessions.first?.connection.id + } + } + + private func restoreAndSynchronizeState() { + viewModel.restoreExpansionState(projectID: projectStore.selectedProject?.id, sessions: sessions) + synchronizeDefaults() + } + + private func schedulePendingNavigationConsumption() { + Task { @MainActor in + await Task.yield() + if let focus = navigationStore.pendingExplorerFocus { + handleExplorerFocus(focus) + return + } + if let connectionID = navigationStore.pendingExplorerRevealConnectionID { + revealConnection(connectionID) + } + } + } + + private func focusNewSession(_ session: ConnectionSession) { + let serverNodeID = ExperimentalObjectBrowserSidebarViewModel.serverNodeID(connectionID: session.connection.id) + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + viewModel.selectedNodeID = serverNodeID + viewModel.setExpanded(true, nodeID: serverNodeID) + viewModel.setExpanded(true, nodeID: ExperimentalObjectBrowserSidebarViewModel.databasesFolderNodeID(connectionID: session.connection.id)) + viewModel.revealAndPulse(nodeID: serverNodeID) + + Task { @MainActor in + try? await Task.sleep(nanoseconds: 1_500_000_000) + if viewModel.highlightedNodeID == serverNodeID { + viewModel.highlightedNodeID = nil + } + if viewModel.revealedNodeID == serverNodeID { + viewModel.revealedNodeID = nil + } + } + } + + private func revealConnection(_ connectionID: UUID) { + let serverNodeID = ExperimentalObjectBrowserSidebarViewModel.serverNodeID(connectionID: connectionID) + selectedConnectionID = connectionID + viewModel.selectedNodeID = serverNodeID + viewModel.setExpanded(true, nodeID: serverNodeID) + viewModel.revealAndPulse(nodeID: serverNodeID) + navigationStore.pendingExplorerRevealConnectionID = nil + } + + private func handleSelectionChange(_ node: ExperimentalObjectBrowserNode?) { + guard let node else { return } + viewModel.selectedNodeID = node.id + + switch node.row { + case .topSpacer: + break + case .pendingConnection: + break + case .server(let session), + .databasesFolder(let session, _), + .database(let session, _, _), + .objectGroup(let session, _, _, _), + .object(let session, _, _), + .serverFolder(let session, _, _), + .databaseFolder(let session, _, _, _, _), + .databaseSubfolder(let session, _, _, _, _, _), + .databaseNamedItem(let session, _, _, _, _, _), + .securitySection(let session, _, _, _), + .securityLogin(let session, _), + .securityServerRole(let session, _), + .securityCredential(let session, _), + .agentJob(let session, _), + .databaseSnapshot(let session, _), + .linkedServer(let session, _), + .ssisFolder(let session, _), + .serverTrigger(let session, _), + .action(let session, _, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + case .infoLeaf(_, _, _, _), .loading(_, _), .message(_, _, _): + break + } + } + + private func handleActivation(of node: ExperimentalObjectBrowserNode) { + viewModel.selectedNodeID = node.id + + switch node.row { + case .topSpacer: + return + case .pendingConnection: + return + case .server(let session): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + case .databasesFolder(let session, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + case .database(let session, let database, _): + guard database.isAccessible else { return } + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + session.sidebarFocusedDatabase = database.name + case .objectGroup(let session, let databaseName, _, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + session.sidebarFocusedDatabase = databaseName + case .object(let session, let databaseName, let object): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + session.sidebarFocusedDatabase = databaseName + viewModel.selectedNodeID = ExplorerSidebarIdentity.object( + connectionID: session.connection.id, + databaseName: databaseName, + objectID: object.id + ) + case .serverFolder(let session, _, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + case .databaseFolder(let session, let databaseName, _, _, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + session.sidebarFocusedDatabase = databaseName + case .databaseSubfolder(let session, let databaseName, _, _, _, _), + .databaseNamedItem(let session, let databaseName, _, _, _, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + session.sidebarFocusedDatabase = databaseName + case .securitySection(let session, _, _, _), + .securityLogin(let session, _), + .securityServerRole(let session, _), + .securityCredential(let session, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + case .agentJob(let session, _), + .databaseSnapshot(let session, _), + .linkedServer(let session, _), + .ssisFolder(let session, _), + .serverTrigger(let session, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + case .action(let session, let action, _): + selectedConnectionID = session.connection.id + environmentState.sessionGroup.setActiveSession(session.id) + perform(action: action, session: session) + case .infoLeaf(_, _, _, _), .loading(_, _), .message(_, _, _): + break + } + } + + private func handleExpansionChange(of node: ExperimentalObjectBrowserNode, isExpanded: Bool) { + withAnimation(.snappy(duration: 0.18, extraBounce: 0)) { + viewModel.setExpanded(isExpanded, nodeID: node.id) + } + + switch node.row { + case .database(let session, let database, _): + if isExpanded { + loadSchemaIfNeeded(databaseName: database.name, session: session) + } + case .serverFolder(let session, let kind, _): + guard isExpanded else { break } + switch kind { + case .agentJobs: + if (viewModel.agentJobsBySession[session.connection.id] ?? []).isEmpty, + !(viewModel.agentJobsLoadingBySession[session.connection.id] ?? false) { + loadAgentJobs(session: session) + } + case .databaseSnapshots: + if (viewModel.databaseSnapshotsBySession[session.connection.id] ?? []).isEmpty, + !(viewModel.databaseSnapshotsLoadingBySession[session.connection.id] ?? false) { + loadDatabaseSnapshots(session: session) + } + case .ssis: + if (viewModel.ssisFoldersBySession[session.connection.id] ?? []).isEmpty, + !(viewModel.ssisLoadingBySession[session.connection.id] ?? false) { + Task { await loadSSISFoldersAsync(session: session) } + } + case .linkedServers: + if (viewModel.linkedServersBySession[session.connection.id] ?? []).isEmpty, + !(viewModel.linkedServersLoadingBySession[session.connection.id] ?? false) { + loadLinkedServers(session: session) + } + case .serverTriggers: + if (viewModel.serverTriggersBySession[session.connection.id] ?? []).isEmpty, + !(viewModel.serverTriggersLoadingBySession[session.connection.id] ?? false) { + loadServerTriggers(session: session) + } + case .security: + if isExpanded { + loadServerSecurityIfNeeded(session: session) + } + case .management: + break + } + case .databaseFolder(let session, let databaseName, let kind, _, _): + guard isExpanded else { break } + guard let database = session.databaseStructure?.databases.first(where: { $0.name == databaseName }) else { break } + switch kind { + case .security: + loadDatabaseSecurityIfNeeded(database: database, session: session) + case .databaseTriggers: + if (viewModel.dbDDLTriggersByDB[viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: databaseName)] ?? []).isEmpty, + !(viewModel.dbDDLTriggersLoadingByDB[viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: databaseName)] ?? false) { + loadDatabaseDDLTriggers(database: database, session: session) + } + case .serviceBroker: + if viewModel.serviceBrokerQueuesByDB[viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: databaseName)] == nil, + !(viewModel.serviceBrokerLoadingByDB[viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: databaseName)] ?? false) { + loadServiceBrokerData(database: database, session: session) + } + case .externalResources: + if viewModel.externalDataSourcesByDB[viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: databaseName)] == nil, + !(viewModel.externalResourcesLoadingByDB[viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: databaseName)] ?? false) { + loadExternalResources(database: database, session: session) + } + } + default: + break + } + } + + private func perform(action: ExperimentalObjectBrowserActionKind, session: ConnectionSession) { + let connectionID = session.connection.id + + switch action { + case .maintenance: + environmentState.openMaintenanceTab(connectionID: connectionID) + case .serverProperties: + environmentState.openServerPropertiesTab(connectionID: connectionID) + case .activityMonitor: + environmentState.openActivityMonitorTab(connectionID: connectionID) + case .extendedEvents: + environmentState.openActivityMonitorTab(connectionID: connectionID, section: "XEvents") + case .databaseMail: + let value = environmentState.prepareDatabaseMailEditorWindow(connectionSessionID: connectionID) + openWindow(id: DatabaseMailEditorWindow.sceneID, value: value) + case .sqlProfiler: + environmentState.openActivityMonitorTab(connectionID: connectionID, section: "Profiler") + case .resourceGovernor: + environmentState.openResourceGovernorTab(connectionID: connectionID) + case .tuningAdvisor: + environmentState.openTuningAdvisorTab(connectionID: connectionID) + case .policyManagement: + environmentState.openPolicyManagementTab(connectionID: connectionID) + case .sqlServerLogs: + environmentState.openErrorLogTab(connectionID: connectionID) + case .openJobQueue: + environmentState.openJobQueueTab(for: session) + } + } + + private func loadSchemaIfNeeded(databaseName: String, session: ConnectionSession) { + let freshness = session.metadataFreshness(forDatabase: databaseName) + switch freshness { + case .cached, .listOnly: + break + case .refreshing, .live, .failed: + return + } + guard session.beginSchemaLoad(forDatabase: databaseName) else { return } + + Task { @MainActor in + session.markMetadataRefreshStarted(forDatabase: databaseName) + defer { session.finishSchemaLoad(forDatabase: databaseName) } + await environmentState.loadSchemaForDatabase(databaseName, connectionSession: session) + } + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarViewModel+Persistence.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarViewModel+Persistence.swift new file mode 100644 index 000000000..9bc56d3f9 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarViewModel+Persistence.swift @@ -0,0 +1,56 @@ +import Foundation + +extension ExperimentalObjectBrowserSidebarViewModel { + private static let sidebarStateDefaultsKey = "experimentalObjectBrowser.sidebarStateByProject" + + struct PersistedSidebarState: Codable { + let expandedNodeIDs: [String] + let initializedConnectionIDs: [UUID] + } + + func restoreExpansionState(projectID: UUID?, sessions: [ConnectionSession]) { + let payloads = loadPersistedStatePayloads() + let storageKey = persistenceStorageKey(for: projectID) + guard let payload = payloads[storageKey] else { + initializedConnectionIDs = [] + expandedNodeIDs = [] + return + } + + let sessionIDs = Set(sessions.map(\.connection.id)) + let restoredInitialized = Set(payload.initializedConnectionIDs).intersection(sessionIDs) + initializedConnectionIDs = restoredInitialized + + let validPrefixes = restoredInitialized.map { $0.uuidString } + expandedNodeIDs = Set( + payload.expandedNodeIDs.filter { nodeID in + validPrefixes.contains(where: { nodeID.hasPrefix($0) }) + } + ) + } + + func persistExpansionState(projectID: UUID?) { + var payloads = loadPersistedStatePayloads() + let storageKey = persistenceStorageKey(for: projectID) + + payloads[storageKey] = PersistedSidebarState( + expandedNodeIDs: Array(expandedNodeIDs).sorted(), + initializedConnectionIDs: Array(initializedConnectionIDs).sorted { $0.uuidString < $1.uuidString } + ) + + guard let encoded = try? JSONEncoder().encode(payloads) else { return } + UserDefaults.standard.set(encoded, forKey: Self.sidebarStateDefaultsKey) + } + + private func persistenceStorageKey(for projectID: UUID?) -> String { + projectID?.uuidString ?? "global" + } + + private func loadPersistedStatePayloads() -> [String: PersistedSidebarState] { + guard let data = UserDefaults.standard.data(forKey: Self.sidebarStateDefaultsKey), + let decoded = try? JSONDecoder().decode([String: PersistedSidebarState].self, from: data) else { + return [:] + } + return decoded + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarViewModel.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarViewModel.swift new file mode 100644 index 000000000..b90f1c9af --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSidebarViewModel.swift @@ -0,0 +1,271 @@ +import Foundation +import SQLServerKit + +@MainActor @Observable +final class ExperimentalObjectBrowserSidebarViewModel { + var expandedNodeIDs: Set = [] + var selectedNodeID: String? + var hideOfflineDatabasesBySession: [UUID: Bool] = [:] + var revealedNodeID: String? + var revealRequestID = 0 + var highlightedNodeID: String? + var highlightPulse = false + var agentJobsBySession: [UUID: [AgentJobItem]] = [:] + var agentJobsLoadingBySession: [UUID: Bool] = [:] + var linkedServersBySession: [UUID: [LinkedServerItem]] = [:] + var linkedServersLoadingBySession: [UUID: Bool] = [:] + var ssisFoldersBySession: [UUID: [SQLServerSSISFolder]] = [:] + var ssisLoadingBySession: [UUID: Bool] = [:] + var databaseSnapshotsBySession: [UUID: [SQLServerDatabaseSnapshot]] = [:] + var databaseSnapshotsLoadingBySession: [UUID: Bool] = [:] + var serverTriggersBySession: [UUID: [ServerTriggerItem]] = [:] + var serverTriggersLoadingBySession: [UUID: Bool] = [:] + var securityLoginsBySession: [UUID: [SecurityLoginItem]] = [:] + var securityServerRolesBySession: [UUID: [SecurityServerRoleItem]] = [:] + var securityCredentialsBySession: [UUID: [SecurityCredentialItem]] = [:] + var securityServerLoadingBySession: [UUID: Bool] = [:] + var dbSecurityUsersByDB: [String: [SecurityUserItem]] = [:] + var dbSecurityRolesByDB: [String: [SecurityDatabaseRoleItem]] = [:] + var dbSecurityAppRolesByDB: [String: [SecurityAppRoleItem]] = [:] + var dbSecuritySchemasByDB: [String: [SecuritySchemaItem]] = [:] + var dbSecurityLoadingByDB: [String: Bool] = [:] + var dbDDLTriggersByDB: [String: [DatabaseDDLTriggerItem]] = [:] + var dbDDLTriggersLoadingByDB: [String: Bool] = [:] + var serviceBrokerLoadingByDB: [String: Bool] = [:] + var serviceBrokerMessageTypesByDB: [String: [String]] = [:] + var serviceBrokerContractsByDB: [String: [String]] = [:] + var serviceBrokerQueuesByDB: [String: [String]] = [:] + var serviceBrokerServicesByDB: [String: [String]] = [:] + var serviceBrokerRoutesByDB: [String: [String]] = [:] + var serviceBrokerBindingsByDB: [String: [String]] = [:] + var externalResourcesLoadingByDB: [String: Bool] = [:] + var externalDataSourcesByDB: [String: [String]] = [:] + var externalTablesByDB: [String: [String]] = [:] + var externalFileFormatsByDB: [String: [String]] = [:] + + @ObservationIgnored var initializedConnectionIDs: Set = [] + + func synchronizeDefaults( + sessions: [ConnectionSession], + autoExpandSectionsForDatabaseType: (DatabaseType) -> Set + ) { + let validConnectionIDs = Set(sessions.map(\.connection.id)) + initializedConnectionIDs = initializedConnectionIDs.intersection(validConnectionIDs) + var expanded = expandedNodeIDs + + for session in sessions where !initializedConnectionIDs.contains(session.connection.id) { + initializedConnectionIDs.insert(session.connection.id) + + expanded.insert(Self.serverNodeID(connectionID: session.connection.id)) + + let autoExpand = autoExpandSectionsForDatabaseType(session.connection.databaseType) + if autoExpand.contains(.databases) { + expanded.insert(Self.databasesFolderNodeID(connectionID: session.connection.id)) + } + if autoExpand.contains(.security) { + expanded.insert( + Self.serverFolderNodeID(connectionID: session.connection.id, kind: .security) + ) + } + if autoExpand.contains(.management) { + expanded.insert( + Self.serverFolderNodeID(connectionID: session.connection.id, kind: .management) + ) + } + } + + expandedNodeIDs = expanded + } + + func setExpanded(_ isExpanded: Bool, nodeID: String) { + var expanded = expandedNodeIDs + if isExpanded { + expanded.insert(nodeID) + } else { + expanded.remove(nodeID) + } + expandedNodeIDs = expanded + } + + func toggleExpanded(nodeID: String) -> Bool { + var expanded = expandedNodeIDs + if expanded.contains(nodeID) { + expanded.remove(nodeID) + expandedNodeIDs = expanded + return false + } else { + expanded.insert(nodeID) + expandedNodeIDs = expanded + return true + } + } + + func isExpanded(_ nodeID: String) -> Bool { + expandedNodeIDs.contains(nodeID) + } + + func revealAndPulse(nodeID: String) { + revealedNodeID = nodeID + revealRequestID &+= 1 + highlightedNodeID = nodeID + highlightPulse.toggle() + } + + struct LinkedServerItem: Identifiable, Hashable { + let id: String + let name: String + let provider: String + let dataSource: String + let product: String + let isDataAccessEnabled: Bool + } + + struct AgentJobItem: Identifiable, Hashable { + let id: String + let name: String + let enabled: Bool + let lastOutcome: String? + } + + struct ServerTriggerItem: Identifiable, Hashable { + let id: String + let name: String + let isDisabled: Bool + let typeDescription: String + let events: [String] + } + + struct SecurityLoginItem: Identifiable, Hashable { + let id: String + let name: String + let loginType: String + let isDisabled: Bool + } + + struct SecurityServerRoleItem: Identifiable, Hashable { + let id: String + let name: String + let isFixed: Bool + } + + struct SecurityCredentialItem: Identifiable, Hashable { + let id: String + let name: String + let identity: String + } + + struct SecurityUserItem: Identifiable, Hashable { + let id: String + let name: String + let userType: String + let defaultSchema: String? + } + + struct SecurityDatabaseRoleItem: Identifiable, Hashable { + let id: String + let name: String + let isFixed: Bool + let owner: String? + } + + struct SecurityAppRoleItem: Identifiable, Hashable { + let id: String + let name: String + let defaultSchema: String? + } + + struct SecuritySchemaItem: Identifiable, Hashable { + let id: String + let name: String + let owner: String? + } + + struct DatabaseDDLTriggerItem: Identifiable, Hashable { + let id: String + let name: String + let isDisabled: Bool + let events: [String] + } +} + +extension ExperimentalObjectBrowserSidebarViewModel { + static func serverNodeID(connectionID: UUID) -> String { + "\(connectionID.uuidString)#server" + } + + static func databasesFolderNodeID(connectionID: UUID) -> String { + "\(connectionID.uuidString)#folder#databases" + } + + static func databaseNodeID(connectionID: UUID, databaseName: String) -> String { + ExplorerSidebarIdentity.database(connectionID: connectionID, databaseName: databaseName) + } + + static func objectGroupNodeID( + connectionID: UUID, + databaseName: String, + objectType: SchemaObjectInfo.ObjectType + ) -> String { + "\(connectionID.uuidString)#db#\(databaseName)#group#\(objectType.rawValue)" + } + + static func serverFolderNodeID( + connectionID: UUID, + kind: ExperimentalObjectBrowserServerFolderKind + ) -> String { + "\(connectionID.uuidString)#server-folder#\(kind.rawValue)" + } + + static func securitySectionNodeID( + connectionID: UUID, + kind: ExperimentalObjectBrowserSecuritySectionKind, + parentID: String + ) -> String { + "\(parentID)#security-section#\(connectionID.uuidString)#\(kind.rawValue)" + } + + static func securityLeafNodeID( + connectionID: UUID, + parentID: String, + kind: ExperimentalObjectBrowserSecuritySectionKind, + name: String + ) -> String { + "\(parentID)#security-leaf#\(connectionID.uuidString)#\(kind.rawValue)#\(name)" + } + + static func actionNodeID( + connectionID: UUID, + parentID: String?, + kind: ExperimentalObjectBrowserActionKind + ) -> String { + "\(parentID ?? connectionID.uuidString)#action#\(kind.rawValue)" + } + + static func databaseFolderNodeID( + connectionID: UUID, + databaseName: String, + kind: ExperimentalObjectBrowserDatabaseFolderKind + ) -> String { + "\(connectionID.uuidString)#db#\(databaseName)#folder#\(kind.rawValue)" + } + + static func databaseSubfolderNodeID(parentID: String, title: String) -> String { + "\(parentID)#subfolder#\(title)" + } + + static func databaseItemNodeID(parentID: String, title: String) -> String { + "\(parentID)#item#\(title)" + } + + func databaseStorageKey(connectionID: UUID, databaseName: String) -> String { + "\(connectionID.uuidString)#\(databaseName)" + } + + static func infoNodeID(parentID: String, title: String) -> String { + "\(parentID)#info#\(title)" + } + + static func loadingNodeID(parentID: String) -> String { + "\(parentID)#loading" + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+DatabaseHelpers.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+DatabaseHelpers.swift new file mode 100644 index 000000000..e981baa63 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+DatabaseHelpers.swift @@ -0,0 +1,42 @@ +import Foundation + +extension ExperimentalObjectBrowserSnapshotBuilder { + static func visibleDatabases( + for session: ConnectionSession, + structure: DatabaseStructure?, + settings: GlobalSettings, + hideOffline: Bool + ) -> [DatabaseInfo] { + let hideInaccessible = settings.hideInaccessibleDatabases + return (structure?.databases ?? []) + .filter { !hideInaccessible || $0.isAccessible } + .filter { !hideOffline || $0.isOnline } + .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + static func groupedObjects( + for database: DatabaseInfo, + supportedTypes: [SchemaObjectInfo.ObjectType] + ) -> [SchemaObjectInfo.ObjectType: [SchemaObjectInfo]] { + var grouped: [SchemaObjectInfo.ObjectType: [SchemaObjectInfo]] = [:] + let supportedSet = Set(supportedTypes) + + for schema in database.schemas { + for object in schema.objects where supportedSet.contains(object.type) { + grouped[object.type, default: []].append(object) + } + } + + for ext in database.extensions where supportedSet.contains(.extension) { + grouped[.extension, default: []].append(ext) + } + + for key in grouped.keys { + grouped[key]?.sort { + $0.fullName.localizedCaseInsensitiveCompare($1.fullName) == .orderedAscending + } + } + + return grouped + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+DatabaseSections.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+DatabaseSections.swift new file mode 100644 index 000000000..1f342cd98 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+DatabaseSections.swift @@ -0,0 +1,219 @@ +import Foundation + +extension ExperimentalObjectBrowserSnapshotBuilder { + static func databaseSupplementaryChildren( + for session: ConnectionSession, + database: DatabaseInfo, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + switch session.connection.databaseType { + case .microsoftSQL: + guard database.isOnline else { return [] } + return [ + databaseSecurityFolderNode(for: session, database: database, viewModel: viewModel), + databaseDDLTriggersFolderNode(for: session, database: database, viewModel: viewModel), + serviceBrokerFolderNode(for: session, database: database, viewModel: viewModel), + externalResourcesFolderNode(for: session, database: database, viewModel: viewModel) + ] + case .postgresql, .mysql, .sqlite: + return [] + } + } + + private static func databaseSecurityFolderNode( + for session: ConnectionSession, + database: DatabaseInfo, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> ExperimentalObjectBrowserNode { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + let nodeID = ExperimentalObjectBrowserSidebarViewModel.databaseFolderNodeID( + connectionID: session.connection.id, + databaseName: database.name, + kind: .security + ) + let isLoading = viewModel.dbSecurityLoadingByDB[dbKey] ?? false + let users = viewModel.dbSecurityUsersByDB[dbKey] ?? [] + let roles = viewModel.dbSecurityRolesByDB[dbKey] ?? [] + let appRoles = viewModel.dbSecurityAppRolesByDB[dbKey] ?? [] + let schemas = viewModel.dbSecuritySchemasByDB[dbKey] ?? [] + + let children: [ExperimentalObjectBrowserNode] = [ + databaseSubfolderNode( + session: session, + databaseName: database.name, + parentID: nodeID, + title: "Users", + systemImage: "person", + paletteTitle: "Users", + items: users.map { ($0.name, "person", "Users", $0.defaultSchema) }, + emptyTitle: "No users found" + ), + databaseSubfolderNode( + session: session, + databaseName: database.name, + parentID: nodeID, + title: "Database Roles", + systemImage: "shield", + paletteTitle: "Database Roles", + items: roles.map { ($0.name, "shield", "Database Roles", $0.isFixed ? "Fixed" : nil) }, + emptyTitle: "No database roles found" + ), + databaseSubfolderNode( + session: session, + databaseName: database.name, + parentID: nodeID, + title: "Application Roles", + systemImage: "app.badge", + paletteTitle: "Application Roles", + items: appRoles.map { ($0.name, "app.badge", "Application Roles", $0.defaultSchema) }, + emptyTitle: "No application roles found" + ), + databaseSubfolderNode( + session: session, + databaseName: database.name, + parentID: nodeID, + title: "Schemas", + systemImage: "folder", + paletteTitle: "Schemas", + items: schemas.map { ($0.name, "folder", "Schemas", $0.owner) }, + emptyTitle: "No schemas found" + ) + ] + + return ExperimentalObjectBrowserNode( + id: nodeID, + row: .databaseFolder(session, database.name, .security, count: nil, isLoading: isLoading), + children: children + ) + } + + private static func databaseDDLTriggersFolderNode( + for session: ConnectionSession, + database: DatabaseInfo, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> ExperimentalObjectBrowserNode { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + let nodeID = ExperimentalObjectBrowserSidebarViewModel.databaseFolderNodeID( + connectionID: session.connection.id, + databaseName: database.name, + kind: .databaseTriggers + ) + let items = viewModel.dbDDLTriggersByDB[dbKey] ?? [] + let isLoading = viewModel.dbDDLTriggersLoadingByDB[dbKey] ?? false + let children: [ExperimentalObjectBrowserNode] + + if isLoading && items.isEmpty { + children = [ExperimentalObjectBrowserNode(id: "\(nodeID)#loading", row: .loading("Loading database triggers…", depth: 3))] + } else if items.isEmpty { + children = [ExperimentalObjectBrowserNode(id: "\(nodeID)#empty", row: .infoLeaf("No database triggers", systemImage: "bolt", paletteTitle: "Database Triggers", depth: 3))] + } else { + children = items.map { + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.databaseItemNodeID(parentID: nodeID, title: $0.name), + row: .databaseNamedItem(session, database.name, title: $0.name, systemImage: "bolt", paletteTitle: "Database Triggers", detail: $0.isDisabled ? "Disabled" : nil) + ) + } + } + + return ExperimentalObjectBrowserNode( + id: nodeID, + row: .databaseFolder(session, database.name, .databaseTriggers, count: items.isEmpty ? nil : items.count, isLoading: isLoading), + children: children + ) + } + + private static func serviceBrokerFolderNode( + for session: ConnectionSession, + database: DatabaseInfo, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> ExperimentalObjectBrowserNode { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + let nodeID = ExperimentalObjectBrowserSidebarViewModel.databaseFolderNodeID( + connectionID: session.connection.id, + databaseName: database.name, + kind: .serviceBroker + ) + let isLoading = viewModel.serviceBrokerLoadingByDB[dbKey] ?? false + let children = [ + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "Message Types", systemImage: "tray", paletteTitle: "Service Broker", items: (viewModel.serviceBrokerMessageTypesByDB[dbKey] ?? []).map { ($0, "doc", "Service Broker", nil) }, emptyTitle: "None"), + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "Contracts", systemImage: "tray", paletteTitle: "Service Broker", items: (viewModel.serviceBrokerContractsByDB[dbKey] ?? []).map { ($0, "doc", "Service Broker", nil) }, emptyTitle: "None"), + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "Queues", systemImage: "tray", paletteTitle: "Service Broker", items: (viewModel.serviceBrokerQueuesByDB[dbKey] ?? []).map { ($0, "doc", "Service Broker", nil) }, emptyTitle: "None"), + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "Services", systemImage: "tray", paletteTitle: "Service Broker", items: (viewModel.serviceBrokerServicesByDB[dbKey] ?? []).map { ($0, "doc", "Service Broker", nil) }, emptyTitle: "None"), + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "Routes", systemImage: "tray", paletteTitle: "Service Broker", items: (viewModel.serviceBrokerRoutesByDB[dbKey] ?? []).map { ($0, "doc", "Service Broker", nil) }, emptyTitle: "None"), + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "Remote Service Bindings", systemImage: "tray", paletteTitle: "Service Broker", items: (viewModel.serviceBrokerBindingsByDB[dbKey] ?? []).map { ($0, "doc", "Service Broker", nil) }, emptyTitle: "None") + ] + + return ExperimentalObjectBrowserNode( + id: nodeID, + row: .databaseFolder(session, database.name, .serviceBroker, count: nil, isLoading: isLoading), + children: children + ) + } + + private static func externalResourcesFolderNode( + for session: ConnectionSession, + database: DatabaseInfo, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> ExperimentalObjectBrowserNode { + let dbKey = viewModel.databaseStorageKey(connectionID: session.connection.id, databaseName: database.name) + let nodeID = ExperimentalObjectBrowserSidebarViewModel.databaseFolderNodeID( + connectionID: session.connection.id, + databaseName: database.name, + kind: .externalResources + ) + let isLoading = viewModel.externalResourcesLoadingByDB[dbKey] ?? false + let children = [ + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "External Data Sources", systemImage: "externaldrive", paletteTitle: "External Resources", items: (viewModel.externalDataSourcesByDB[dbKey] ?? []).map { ($0, "doc", "External Resources", nil) }, emptyTitle: "None"), + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "External Tables", systemImage: "externaldrive", paletteTitle: "External Resources", items: (viewModel.externalTablesByDB[dbKey] ?? []).map { ($0, "doc", "External Resources", nil) }, emptyTitle: "None"), + databaseSubfolderNode(session: session, databaseName: database.name, parentID: nodeID, title: "External File Formats", systemImage: "externaldrive", paletteTitle: "External Resources", items: (viewModel.externalFileFormatsByDB[dbKey] ?? []).map { ($0, "doc", "External Resources", nil) }, emptyTitle: "None") + ] + + return ExperimentalObjectBrowserNode( + id: nodeID, + row: .databaseFolder(session, database.name, .externalResources, count: nil, isLoading: isLoading), + children: children + ) + } + + private static func databaseSubfolderNode( + session: ConnectionSession, + databaseName: String, + parentID: String, + title: String, + systemImage: String, + paletteTitle: String, + items: [(title: String, systemImage: String, paletteTitle: String, detail: String?)], + emptyTitle: String + ) -> ExperimentalObjectBrowserNode { + let nodeID = ExperimentalObjectBrowserSidebarViewModel.databaseSubfolderNodeID(parentID: parentID, title: title) + let children: [ExperimentalObjectBrowserNode] + if items.isEmpty { + children = [ + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.databaseItemNodeID(parentID: nodeID, title: emptyTitle), + row: .infoLeaf(emptyTitle, systemImage: systemImage, paletteTitle: paletteTitle, depth: 4) + ) + ] + } else { + children = items.map { + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.databaseItemNodeID(parentID: nodeID, title: $0.title), + row: .databaseNamedItem( + session, + databaseName, + title: $0.title, + systemImage: $0.systemImage, + paletteTitle: $0.paletteTitle, + detail: $0.detail + ) + ) + } + } + + return ExperimentalObjectBrowserNode( + id: nodeID, + row: .databaseSubfolder(session, databaseName, title: title, systemImage: systemImage, paletteTitle: paletteTitle, count: items.isEmpty ? nil : items.count), + children: children + ) + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+Security.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+Security.swift new file mode 100644 index 000000000..1ce12f1c3 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+Security.swift @@ -0,0 +1,328 @@ +import Foundation + +extension ExperimentalObjectBrowserSnapshotBuilder { + static func securityChildren( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let connectionID = session.connection.id + let parentID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: connectionID, + kind: .security + ) + let isLoading = viewModel.securityServerLoadingBySession[connectionID] ?? false + + switch session.connection.databaseType { + case .microsoftSQL: + return mssqlSecurityChildren( + for: session, + parentID: parentID, + isLoading: isLoading, + viewModel: viewModel + ) + case .postgresql: + return postgresSecurityChildren( + for: session, + parentID: parentID, + isLoading: isLoading, + viewModel: viewModel + ) + case .mysql, .sqlite: + return [] + } + } + + private static func mssqlSecurityChildren( + for session: ConnectionSession, + parentID: String, + isLoading: Bool, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let connectionID = session.connection.id + let loginsSectionID = ExperimentalObjectBrowserSidebarViewModel.securitySectionNodeID( + connectionID: connectionID, + kind: .logins, + parentID: parentID + ) + let serverRolesSectionID = ExperimentalObjectBrowserSidebarViewModel.securitySectionNodeID( + connectionID: connectionID, + kind: .serverRoles, + parentID: parentID + ) + let credentialsSectionID = ExperimentalObjectBrowserSidebarViewModel.securitySectionNodeID( + connectionID: connectionID, + kind: .credentials, + parentID: parentID + ) + let allLogins = viewModel.securityLoginsBySession[connectionID] ?? [] + let standardLogins = allLogins.filter { !certificateLoginTypes.contains($0.loginType) } + let certificateLogins = allLogins.filter { certificateLoginTypes.contains($0.loginType) } + let serverRoles = viewModel.securityServerRolesBySession[connectionID] ?? [] + let credentials = viewModel.securityCredentialsBySession[connectionID] ?? [] + + return [ + securitySectionNode( + .logins, + session: session, + count: standardLogins.count, + isLoading: isLoading, + parentID: parentID, + children: standardLoginChildren( + for: session, + parentID: loginsSectionID, + items: standardLogins, + certificateLogins: certificateLogins, + isLoading: isLoading + ) + ), + securitySectionNode( + .serverRoles, + session: session, + count: serverRoles.count, + isLoading: isLoading, + parentID: parentID, + children: serverRoleChildren(for: session, parentID: serverRolesSectionID, items: serverRoles, isLoading: isLoading) + ), + securitySectionNode( + .credentials, + session: session, + count: credentials.count, + isLoading: isLoading, + parentID: parentID, + children: credentialChildren(for: session, parentID: credentialsSectionID, items: credentials, isLoading: isLoading) + ) + ] + } + + private static func postgresSecurityChildren( + for session: ConnectionSession, + parentID: String, + isLoading: Bool, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let connectionID = session.connection.id + let loginRolesSectionID = ExperimentalObjectBrowserSidebarViewModel.securitySectionNodeID( + connectionID: connectionID, + kind: .pgLoginRoles, + parentID: parentID + ) + let groupRolesSectionID = ExperimentalObjectBrowserSidebarViewModel.securitySectionNodeID( + connectionID: connectionID, + kind: .pgGroupRoles, + parentID: parentID + ) + let allRoles = viewModel.securityLoginsBySession[connectionID] ?? [] + let loginRoles = allRoles.filter { $0.loginType.contains("Login") || $0.loginType.contains("Superuser") } + let groupRoles = allRoles.filter { $0.loginType == "Group Role" } + + return [ + securitySectionNode( + .pgLoginRoles, + session: session, + count: loginRoles.count, + isLoading: isLoading, + parentID: parentID, + children: securityLoginChildren( + for: session, + parentID: loginRolesSectionID, + sectionKind: .pgLoginRoles, + items: loginRoles, + emptyTitle: "No login roles found", + isLoading: isLoading + ) + ), + securitySectionNode( + .pgGroupRoles, + session: session, + count: groupRoles.count, + isLoading: isLoading, + parentID: parentID, + children: securityLoginChildren( + for: session, + parentID: groupRolesSectionID, + sectionKind: .pgGroupRoles, + items: groupRoles, + emptyTitle: "No group roles found", + isLoading: isLoading + ) + ) + ] + } + + private static func standardLoginChildren( + for session: ConnectionSession, + parentID: String, + items: [ExperimentalObjectBrowserSidebarViewModel.SecurityLoginItem], + certificateLogins: [ExperimentalObjectBrowserSidebarViewModel.SecurityLoginItem], + isLoading: Bool + ) -> [ExperimentalObjectBrowserNode] { + let connectionID = session.connection.id + let loginsParentID = ExperimentalObjectBrowserSidebarViewModel.securitySectionNodeID( + connectionID: connectionID, + kind: .logins, + parentID: parentID + ) + var children = securityLoginChildren( + for: session, + parentID: loginsParentID, + sectionKind: .logins, + items: items, + emptyTitle: "No logins found", + isLoading: isLoading + ) + + if !certificateLogins.isEmpty { + children.append( + securitySectionNode( + .certificateLogins, + session: session, + count: certificateLogins.count, + isLoading: false, + parentID: loginsParentID, + children: securityLoginChildren( + for: session, + parentID: loginsParentID, + sectionKind: .certificateLogins, + items: certificateLogins, + emptyTitle: "No certificate logins found", + isLoading: isLoading + ) + ) + ) + } + + return children + } + + private static func securityLoginChildren( + for session: ConnectionSession, + parentID: String, + sectionKind: ExperimentalObjectBrowserSecuritySectionKind, + items: [ExperimentalObjectBrowserSidebarViewModel.SecurityLoginItem], + emptyTitle: String, + isLoading: Bool + ) -> [ExperimentalObjectBrowserNode] { + if isLoading && items.isEmpty { + return [ + ExperimentalObjectBrowserNode( + id: "\(parentID)#loading", + row: .loading("Loading \(sectionKind.title.lowercased())…", depth: 2) + ) + ] + } + if items.isEmpty { + return [ + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.infoNodeID(parentID: parentID, title: emptyTitle), + row: .infoLeaf(emptyTitle, systemImage: sectionKind.systemImage, paletteTitle: sectionKind.title, depth: 2) + ) + ] + } + + return items.map { + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.securityLeafNodeID( + connectionID: session.connection.id, + parentID: parentID, + kind: sectionKind, + name: $0.name + ), + row: .securityLogin(session, $0) + ) + } + } + + private static func serverRoleChildren( + for session: ConnectionSession, + parentID: String, + items: [ExperimentalObjectBrowserSidebarViewModel.SecurityServerRoleItem], + isLoading: Bool + ) -> [ExperimentalObjectBrowserNode] { + if isLoading && items.isEmpty { + return [ + ExperimentalObjectBrowserNode( + id: "\(parentID)#loading", + row: .loading("Loading server roles…", depth: 2) + ) + ] + } + if items.isEmpty { + return [ + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.infoNodeID(parentID: parentID, title: "No server roles found"), + row: .infoLeaf("No server roles found", systemImage: "shield", paletteTitle: "Server Roles", depth: 2) + ) + ] + } + + return items.map { + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.securityLeafNodeID( + connectionID: session.connection.id, + parentID: parentID, + kind: .serverRoles, + name: $0.name + ), + row: .securityServerRole(session, $0) + ) + } + } + + private static func credentialChildren( + for session: ConnectionSession, + parentID: String, + items: [ExperimentalObjectBrowserSidebarViewModel.SecurityCredentialItem], + isLoading: Bool + ) -> [ExperimentalObjectBrowserNode] { + if isLoading && items.isEmpty { + return [ + ExperimentalObjectBrowserNode( + id: "\(parentID)#loading", + row: .loading("Loading credentials…", depth: 2) + ) + ] + } + if items.isEmpty { + return [ + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.infoNodeID(parentID: parentID, title: "No credentials found"), + row: .infoLeaf("No credentials found", systemImage: "key", paletteTitle: "Credentials", depth: 2) + ) + ] + } + + return items.map { + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.securityLeafNodeID( + connectionID: session.connection.id, + parentID: parentID, + kind: .credentials, + name: $0.name + ), + row: .securityCredential(session, $0) + ) + } + } + + private static func securitySectionNode( + _ kind: ExperimentalObjectBrowserSecuritySectionKind, + session: ConnectionSession, + count: Int, + isLoading: Bool, + parentID: String, + children: [ExperimentalObjectBrowserNode] + ) -> ExperimentalObjectBrowserNode { + let sectionID = ExperimentalObjectBrowserSidebarViewModel.securitySectionNodeID( + connectionID: session.connection.id, + kind: kind, + parentID: parentID + ) + return ExperimentalObjectBrowserNode( + id: sectionID, + row: .securitySection(session, kind, count: count, isLoading: isLoading), + children: children + ) + } + + private static let certificateLoginTypes: Set = ["Certificate", "Asymmetric Key"] +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+ServerSections.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+ServerSections.swift new file mode 100644 index 000000000..43a008b8e --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot+ServerSections.swift @@ -0,0 +1,272 @@ +import Foundation +import SQLServerKit + +extension ExperimentalObjectBrowserSnapshotBuilder { + static func serverSupplementaryChildren( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + switch session.connection.databaseType { + case .microsoftSQL: + return [ + serverFolderNode(.security, session: session, count: nil, children: securityChildren(for: session, viewModel: viewModel)), + serverFolderNode(.databaseSnapshots, session: session, count: snapshotCount(for: session, viewModel: viewModel), children: snapshotChildren(for: session, viewModel: viewModel)), + serverFolderNode(.agentJobs, session: session, count: agentJobCount(for: session, viewModel: viewModel), children: agentJobChildren(for: session, viewModel: viewModel)), + serverFolderNode(.management, session: session, count: nil, children: managementChildren(for: session)), + serverFolderNode(.ssis, session: session, count: ssisCount(for: session, viewModel: viewModel), children: ssisChildren(for: session, viewModel: viewModel)), + serverFolderNode(.linkedServers, session: session, count: linkedServerCount(for: session, viewModel: viewModel), children: linkedServerChildren(for: session, viewModel: viewModel)), + serverFolderNode(.serverTriggers, session: session, count: serverTriggerCount(for: session, viewModel: viewModel), children: serverTriggerChildren(for: session, viewModel: viewModel)) + ] + case .postgresql: + return [ + serverFolderNode(.security, session: session, count: nil, children: securityChildren(for: session, viewModel: viewModel)) + ] + case .mysql: + return [ + actionNode(.maintenance, session: session, depth: 0), + actionNode(.serverProperties, session: session, depth: 0), + actionNode(.activityMonitor, session: session, depth: 0) + ] + case .sqlite: + return [ + actionNode(.maintenance, session: session, depth: 0) + ] + } + } + + private static func serverFolderNode( + _ kind: ExperimentalObjectBrowserServerFolderKind, + session: ConnectionSession, + count: Int?, + children: [ExperimentalObjectBrowserNode] + ) -> ExperimentalObjectBrowserNode { + let nodeID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: session.connection.id, + kind: kind + ) + return ExperimentalObjectBrowserNode( + id: nodeID, + row: .serverFolder(session, kind, count: count), + children: children + ) + } + + private static func managementChildren(for session: ConnectionSession) -> [ExperimentalObjectBrowserNode] { + let parentID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: session.connection.id, + kind: .management + ) + + return [ + actionNode(.extendedEvents, session: session, depth: 1, parentID: parentID), + actionNode(.databaseMail, session: session, depth: 1, parentID: parentID), + actionNode(.sqlProfiler, session: session, depth: 1, parentID: parentID), + actionNode(.resourceGovernor, session: session, depth: 1, parentID: parentID), + actionNode(.tuningAdvisor, session: session, depth: 1, parentID: parentID), + actionNode(.policyManagement, session: session, depth: 1, parentID: parentID), + actionNode(.activityMonitor, session: session, depth: 1, parentID: parentID), + actionNode(.sqlServerLogs, session: session, depth: 1, parentID: parentID) + ] + } + + private static func snapshotCount( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> Int? { + let items = viewModel.databaseSnapshotsBySession[session.connection.id] ?? [] + return items.isEmpty ? nil : items.count + } + + private static func snapshotChildren( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let parentID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: session.connection.id, + kind: .databaseSnapshots + ) + let items = viewModel.databaseSnapshotsBySession[session.connection.id] ?? [] + let isLoading = viewModel.databaseSnapshotsLoadingBySession[session.connection.id] ?? false + + if isLoading { + return [ExperimentalObjectBrowserNode(id: "\(parentID)#loading", row: .loading("Loading snapshots…", depth: 1))] + } + if items.isEmpty { + return [infoNode(title: "No snapshots", systemImage: "camera", paletteTitle: "Database Snapshots", depth: 1, parentID: parentID)] + } + return items.map { + ExperimentalObjectBrowserNode( + id: "\(parentID)#snapshot#\($0.name)", + row: .databaseSnapshot(session, $0) + ) + } + } + + private static func agentJobCount( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> Int? { + let items = viewModel.agentJobsBySession[session.connection.id] ?? [] + return items.isEmpty ? nil : items.count + } + + private static func agentJobChildren( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let parentID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: session.connection.id, + kind: .agentJobs + ) + let items = viewModel.agentJobsBySession[session.connection.id] ?? [] + let isLoading = viewModel.agentJobsLoadingBySession[session.connection.id] ?? false + var children: [ExperimentalObjectBrowserNode] = [ + actionNode(.openJobQueue, session: session, depth: 1, parentID: parentID) + ] + + if isLoading { + children.append(ExperimentalObjectBrowserNode(id: "\(parentID)#loading", row: .loading("Loading jobs…", depth: 1))) + return children + } + if items.isEmpty { + children.append(infoNode(title: "No jobs found", systemImage: "clock", paletteTitle: "Agent Jobs", depth: 1, parentID: parentID)) + return children + } + children.append(contentsOf: items.map { + ExperimentalObjectBrowserNode( + id: "\(parentID)#job#\($0.id)", + row: .agentJob(session, $0) + ) + }) + return children + } + + private static func ssisCount( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> Int? { + let items = viewModel.ssisFoldersBySession[session.connection.id] ?? [] + return items.isEmpty ? nil : items.count + } + + private static func ssisChildren( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let parentID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: session.connection.id, + kind: .ssis + ) + let items = viewModel.ssisFoldersBySession[session.connection.id] ?? [] + let isLoading = viewModel.ssisLoadingBySession[session.connection.id] ?? false + + if isLoading { + return [ExperimentalObjectBrowserNode(id: "\(parentID)#loading", row: .loading("Loading catalogs…", depth: 1))] + } + if items.isEmpty { + return [infoNode(title: "No catalogs found", systemImage: "shippingbox", paletteTitle: "Integration Services Catalogs", depth: 1, parentID: parentID)] + } + return items.map { + ExperimentalObjectBrowserNode( + id: "\(parentID)#ssis#\($0.name)", + row: .ssisFolder(session, $0) + ) + } + } + + private static func linkedServerCount( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> Int? { + let items = viewModel.linkedServersBySession[session.connection.id] ?? [] + return items.isEmpty ? nil : items.count + } + + private static func linkedServerChildren( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let parentID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: session.connection.id, + kind: .linkedServers + ) + let items = viewModel.linkedServersBySession[session.connection.id] ?? [] + let isLoading = viewModel.linkedServersLoadingBySession[session.connection.id] ?? false + + if isLoading { + return [ExperimentalObjectBrowserNode(id: "\(parentID)#loading", row: .loading("Loading linked servers…", depth: 1))] + } + if items.isEmpty { + return [infoNode(title: "No linked servers", systemImage: "link", paletteTitle: "Linked Servers", depth: 1, parentID: parentID)] + } + return items.map { + ExperimentalObjectBrowserNode( + id: "\(parentID)#linked#\($0.id)", + row: .linkedServer(session, $0) + ) + } + } + + private static func serverTriggerCount( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> Int? { + let items = viewModel.serverTriggersBySession[session.connection.id] ?? [] + return items.isEmpty ? nil : items.count + } + + private static func serverTriggerChildren( + for session: ConnectionSession, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let parentID = ExperimentalObjectBrowserSidebarViewModel.serverFolderNodeID( + connectionID: session.connection.id, + kind: .serverTriggers + ) + let items = viewModel.serverTriggersBySession[session.connection.id] ?? [] + let isLoading = viewModel.serverTriggersLoadingBySession[session.connection.id] ?? false + + if isLoading { + return [ExperimentalObjectBrowserNode(id: "\(parentID)#loading", row: .loading("Loading server triggers…", depth: 1))] + } + if items.isEmpty { + return [infoNode(title: "No server triggers", systemImage: "bolt", paletteTitle: "Server Triggers", depth: 1, parentID: parentID)] + } + return items.map { + ExperimentalObjectBrowserNode( + id: "\(parentID)#trigger#\($0.id)", + row: .serverTrigger(session, $0) + ) + } + } + + private static func actionNode( + _ kind: ExperimentalObjectBrowserActionKind, + session: ConnectionSession, + depth: Int, + parentID: String? = nil + ) -> ExperimentalObjectBrowserNode { + let nodeID = ExperimentalObjectBrowserSidebarViewModel.actionNodeID( + connectionID: session.connection.id, + parentID: parentID, + kind: kind + ) + return ExperimentalObjectBrowserNode( + id: nodeID, + row: .action(session, kind, depth: depth) + ) + } + + private static func infoNode( + title: String, + systemImage: String, + paletteTitle: String, + depth: Int, + parentID: String + ) -> ExperimentalObjectBrowserNode { + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.infoNodeID(parentID: parentID, title: title), + row: .infoLeaf(title, systemImage: systemImage, paletteTitle: paletteTitle, depth: depth) + ) + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot.swift new file mode 100644 index 000000000..a15918bb7 --- /dev/null +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/ExperimentalObjectBrowserSnapshot.swift @@ -0,0 +1,220 @@ +import Foundation + +@MainActor +enum ExperimentalObjectBrowserSnapshotBuilder { + static func buildRoots( + pendingConnections: [PendingConnection], + sessions: [ConnectionSession], + settings: GlobalSettings, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + let topSpacer = ExperimentalObjectBrowserNode( + id: "explorer-lab#top-spacer", + row: .topSpacer(SpacingTokens.xs) + ) + + var rows: [ExperimentalObjectBrowserNode] = [topSpacer] + + for pending in pendingConnections { + rows.append( + ExperimentalObjectBrowserNode( + id: "\(pending.id.uuidString)#pending", + row: .pendingConnection(pending) + ) + ) + } + + if !pendingConnections.isEmpty && !sessions.isEmpty { + rows.append( + ExperimentalObjectBrowserNode( + id: "explorer-lab#pending-gap", + row: .topSpacer(SpacingTokens.xs) + ) + ) + } + + for (index, session) in sessions.enumerated() { + if index > 0 { + rows.append( + ExperimentalObjectBrowserNode( + id: "explorer-lab#server-gap#\(session.connection.id.uuidString)", + row: .topSpacer(SpacingTokens.xs) + ) + ) + } + + let serverID = ExperimentalObjectBrowserSidebarViewModel.serverNodeID(connectionID: session.connection.id) + rows.append( + ExperimentalObjectBrowserNode( + id: serverID, + row: .server(session), + children: serverChildren( + for: session, + settings: settings, + viewModel: viewModel + ) + ) + ) + } + + return rows + } + + static func serverChildren( + for session: ConnectionSession, + settings: GlobalSettings, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + switch session.structureLoadingState { + case .failed(let message): + return [ + ExperimentalObjectBrowserNode( + id: "\(session.connection.id.uuidString)#failed", + row: .message(message ?? "Failed to load", systemImage: "exclamationmark.triangle.fill", depth: 1) + ) + ] + case .idle: + return [ + ExperimentalObjectBrowserNode( + id: "\(session.connection.id.uuidString)#server-loading", + row: .loading("Loading server…", depth: 1) + ) + ] + case .loading where session.databaseStructure == nil: + return [ + ExperimentalObjectBrowserNode( + id: "\(session.connection.id.uuidString)#server-loading", + row: .loading("Loading server…", depth: 1) + ) + ] + default: + let structure = session.databaseStructure + let visibleDatabases = visibleDatabases( + for: session, + structure: structure, + settings: settings, + hideOffline: viewModel.hideOfflineDatabasesBySession[session.connection.id] ?? false + ) + let folderID = ExperimentalObjectBrowserSidebarViewModel.databasesFolderNodeID(connectionID: session.connection.id) + let folderChildren = visibleDatabases.map { + databaseNode( + for: session, + database: $0, + settings: settings, + expandedNodeIDs: viewModel.expandedNodeIDs, + viewModel: viewModel + ) + } + + var children = [ + ExperimentalObjectBrowserNode( + id: folderID, + row: .databasesFolder(session, count: visibleDatabases.count), + children: folderChildren + ) + ] + children.append(contentsOf: serverSupplementaryChildren(for: session, viewModel: viewModel)) + return children + } + } + + private static func databaseNode( + for session: ConnectionSession, + database: DatabaseInfo, + settings: GlobalSettings, + expandedNodeIDs: Set, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> ExperimentalObjectBrowserNode { + let databaseID = ExperimentalObjectBrowserSidebarViewModel.databaseNodeID( + connectionID: session.connection.id, + databaseName: database.name + ) + + let isLoading = session.schemaLoadsInFlight.contains(session.schemaLoadKey(database.name)) + let children = databaseChildren( + for: session, + database: database, + settings: settings, + expandedNodeIDs: expandedNodeIDs, + isLoading: isLoading, + viewModel: viewModel + ) + + return ExperimentalObjectBrowserNode( + id: databaseID, + row: .database(session, database, isLoading: isLoading), + children: children + ) + } + + private static func databaseChildren( + for session: ConnectionSession, + database: DatabaseInfo, + settings: GlobalSettings, + expandedNodeIDs: Set, + isLoading: Bool, + viewModel: ExperimentalObjectBrowserSidebarViewModel + ) -> [ExperimentalObjectBrowserNode] { + if isLoading { + return [ + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.loadingNodeID( + parentID: ExperimentalObjectBrowserSidebarViewModel.databaseNodeID( + connectionID: session.connection.id, + databaseName: database.name + ) + ), + row: .loading("Loading schema…", depth: 2) + ) + ] + } + + guard session.hasLoadedSchema(forDatabase: database.name) else { + return [ + ExperimentalObjectBrowserNode( + id: ExperimentalObjectBrowserSidebarViewModel.loadingNodeID( + parentID: ExperimentalObjectBrowserSidebarViewModel.databaseNodeID( + connectionID: session.connection.id, + databaseName: database.name + ) + ), + row: .loading(session.metadataFreshness(forDatabase: database.name) == .failed ? "Schema refresh failed" : "Expand to load objects…", depth: 2) + ) + ] + } + + let supportedTypes = SchemaObjectInfo.ObjectType.supported(for: session.connection.databaseType) + let snapshot = groupedObjects(for: database, supportedTypes: supportedTypes) + + let objectGroupNodes = supportedTypes.map { type in + let objects = snapshot[type] ?? [] + let groupID = ExperimentalObjectBrowserSidebarViewModel.objectGroupNodeID( + connectionID: session.connection.id, + databaseName: database.name, + objectType: type + ) + let groupChildren = objects.map { + ExperimentalObjectBrowserNode( + id: ExplorerSidebarIdentity.object( + connectionID: session.connection.id, + databaseName: database.name, + objectID: $0.id + ), + row: .object(session, database.name, $0) + ) + } + + return ExperimentalObjectBrowserNode( + id: groupID, + row: .objectGroup(session, database.name, type, count: objects.count), + children: groupChildren + ) + } + + return objectGroupNodes + databaseSupplementaryChildren( + for: session, + database: database, + viewModel: viewModel + ) + } +} diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/SidebarMenuView+Content.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/SidebarMenuView+Content.swift index 53bb690e4..f58487ac1 100644 --- a/Echo/Sources/Features/ObjectBrowser/Views/Components/SidebarMenuView+Content.swift +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/SidebarMenuView+Content.swift @@ -5,6 +5,8 @@ extension SidebarMenu { var contentView: some View { switch selectedNavSection { case .folder: + ExperimentalObjectBrowserSidebarView(selectedConnectionID: $selectedConnectionID) + case .experimentalFolder: ObjectBrowserSidebarView(selectedConnectionID: $selectedConnectionID) case .bookmark: BookmarksSidebarView() diff --git a/Echo/Sources/Features/ObjectBrowser/Views/Components/SidebarMenuView.swift b/Echo/Sources/Features/ObjectBrowser/Views/Components/SidebarMenuView.swift index f4462c4cc..7c37725f1 100644 --- a/Echo/Sources/Features/ObjectBrowser/Views/Components/SidebarMenuView.swift +++ b/Echo/Sources/Features/ObjectBrowser/Views/Components/SidebarMenuView.swift @@ -17,6 +17,7 @@ struct SidebarMenu: View { enum NavSection: String, CaseIterable { case folder = "Explorer" + case experimentalFolder = "Explorer Lab" case bookmark = "Bookmarks" case search = "Search" case clipboard = "Clipboard" @@ -27,6 +28,7 @@ struct SidebarMenu: View { var icon: String { switch self { case .folder: return "folder" + case .experimentalFolder: return "testtube.2" case .bookmark: return "bookmark" case .search: return "magnifyingglass" case .clipboard: return "clipboard" @@ -39,6 +41,7 @@ struct SidebarMenu: View { var activeIcon: String { switch self { case .folder: return "folder.fill" + case .experimentalFolder: return "testtube.2" case .bookmark: return "bookmark.fill" case .search: return "magnifyingglass" case .clipboard: return "clipboard.fill" @@ -89,6 +92,12 @@ struct SidebarMenu: View { selectedNavSection = .folder } } + .onChange(of: navigationStore.pendingExplorerRevealRequestID) { _, _ in + guard navigationStore.pendingExplorerRevealConnectionID != nil else { return } + withAnimation(.easeInOut(duration: 0.2)) { + selectedNavSection = .folder + } + } .onReceive(NotificationCenter.default.publisher(for: .activateSidebarSearch)) { _ in withAnimation(.easeInOut(duration: 0.2)) { selectedNavSection = .search diff --git a/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseContent.swift b/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseContent.swift index 4835c8a9c..a2a7b66e3 100644 --- a/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseContent.swift +++ b/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseContent.swift @@ -5,7 +5,7 @@ extension ObjectBrowserSidebarView { @ViewBuilder func databaseContent(database: DatabaseInfo, session: ConnectionSession, hasSchemas: Bool, proxy: ScrollViewProxy) -> some View { let connID = session.connection.id - let isLoading = viewModel.isDatabaseLoading(connectionID: connID, databaseName: database.name) + let isLoading = session.isRefreshingMetadata(forDatabase: database.name) Group { if hasSchemas { @@ -88,30 +88,33 @@ extension ObjectBrowserSidebarView { private func loadSchemaIfNeeded(connID: UUID, database: DatabaseInfo, session: ConnectionSession) { let hasSchemas = !database.schemas.isEmpty && database.schemas.contains(where: { !$0.objects.isEmpty }) - let isLoading = viewModel.isDatabaseLoading(connectionID: connID, databaseName: database.name) - let alreadyLoaded = viewModel.isDatabaseSchemaLoadedOnce(connectionID: connID, databaseName: database.name) - let needsLoad = !hasSchemas && !isLoading && !alreadyLoaded + let freshness = session.metadataFreshness(forDatabase: database.name) + let isLoading = session.isRefreshingMetadata(forDatabase: database.name) + let needsLoad = switch freshness { + case .cached: + !isLoading + case .listOnly: + !isLoading + case .refreshing, .live, .failed: + false + } guard needsLoad else { return } Task { @MainActor in let loadStart = CFAbsoluteTimeGetCurrent() let structureState = session.structureLoadingState print("[PERF] \(database.name): load started (structureState=\(structureState), existingDBCount=\(session.databaseStructure?.databases.count ?? 0))") - viewModel.setDatabaseLoading(connectionID: connID, databaseName: database.name, loading: true) + session.markMetadataRefreshStarted(forDatabase: database.name) + guard session.beginSchemaLoad(forDatabase: database.name) else { return } await environmentState.loadSchemaForDatabase(database.name, connectionSession: session) + session.finishSchemaLoad(forDatabase: database.name) let loadEnd = CFAbsoluteTimeGetCurrent() print("[PERF] \(database.name): load completed in \(String(format: "%.3f", loadEnd - loadStart))s, UI update pending") - // Only mark as "loaded once" if schemas actually arrived. - // If the load returned empty, don't set the flag — allow retry. let updatedDB = session.databaseStructure?.databases.first(where: { $0.name == database.name }) let gotSchemas = updatedDB?.schemas.contains(where: { !$0.objects.isEmpty }) ?? false - if gotSchemas { - viewModel.setDatabaseLoading(connectionID: connID, databaseName: database.name, loading: false) - } else { - // Clear loading state but DON'T mark as loaded-once — allow future retry - let key = viewModel.pinnedStorageKey(connectionID: connID, databaseName: database.name) - viewModel.databaseSchemaLoadingStates[key] = false + if !gotSchemas && freshness == .cached && hasSchemas { + session.markMetadataRefreshCompleted(forDatabase: database.name, hasSchemas: true) } if session.connection.databaseType == .postgresql { diff --git a/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseContextMenu.swift b/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseContextMenu.swift index 4f6a0df45..40eef1e73 100644 --- a/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseContextMenu.swift +++ b/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseContextMenu.swift @@ -21,12 +21,11 @@ func buildDatabaseNSMenu( // Group 1: Refresh menu.addActionItem("Refresh Schema", systemImage: "arrow.clockwise") { viewModel.ensureDatabaseExpanded(connectionID: connID, databaseName: database.name) - viewModel.setDatabaseLoading(connectionID: connID, databaseName: database.name, loading: true) Task { let handle = AppDirector.shared.activityEngine.begin("Refreshing schema for \(database.name)", connectionSessionID: session.id) + session.markMetadataRefreshStarted(forDatabase: database.name) await environmentState.loadSchemaForDatabase(database.name, connectionSession: session) handle.succeed() - viewModel.setDatabaseLoading(connectionID: connID, databaseName: database.name, loading: false) } } @@ -217,12 +216,11 @@ extension ObjectBrowserSidebarView { // Group 1: Refresh Button { viewModel.ensureDatabaseExpanded(connectionID: connID, databaseName: database.name) - viewModel.setDatabaseLoading(connectionID: connID, databaseName: database.name, loading: true) Task { let handle = AppDirector.shared.activityEngine.begin("Refreshing schema for \(database.name)", connectionSessionID: session.id) + session.markMetadataRefreshStarted(forDatabase: database.name) await environmentState.loadSchemaForDatabase(database.name, connectionSession: session) handle.succeed() - viewModel.setDatabaseLoading(connectionID: connID, databaseName: database.name, loading: false) } } label: { Label("Refresh Schema", systemImage: "arrow.clockwise") @@ -246,12 +244,6 @@ extension ObjectBrowserSidebarView { Label("Postgres Console", systemImage: "terminal") } } - if projectStore.globalSettings.nativePsqlEnabled { - Button {} label: { - Label("Native psql (Coming Soon)", systemImage: "chevron.left.forwardslash.chevron.right") - } - .disabled(true) - } } Divider() diff --git a/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseSections.swift b/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseSections.swift index 97de6093e..7c4991c23 100644 --- a/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseSections.swift +++ b/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/DatabaseSections/ObjectBrowserSidebarView+DatabaseSections.swift @@ -133,7 +133,7 @@ extension ObjectBrowserSidebarView { func databaseHeaderRow(database: DatabaseInfo, session: ConnectionSession, isExpanded: Bool, isSelected: Bool, accentColor: Color) -> some View { let connID = session.connection.id - let isLoading = viewModel.isDatabaseLoading(connectionID: connID, databaseName: database.name) + let isLoading = session.isRefreshingMetadata(forDatabase: database.name) let isAvailable = database.isOnline && database.isAccessible let expandedBinding = Binding( get: { isExpanded }, diff --git a/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/ObjectBrowserSidebarView+ContextMenus.swift b/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/ObjectBrowserSidebarView+ContextMenus.swift index cccdfc8f1..ccd1ed917 100644 --- a/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/ObjectBrowserSidebarView+ContextMenus.swift +++ b/Echo/Sources/Features/ObjectBrowser/Views/ObjectBrowserSidebar/ObjectBrowserSidebarView+ContextMenus.swift @@ -11,12 +11,11 @@ extension ObjectBrowserSidebarView { // Group 1: Refresh Button { - viewModel.setDatabaseLoading(connectionID: connID, databaseName: database.name, loading: true) Task { let handle = AppDirector.shared.activityEngine.begin("Refreshing \(type.pluralDisplayName)", connectionSessionID: session.id) + session.markMetadataRefreshStarted(forDatabase: database.name) await environmentState.loadSchemaForDatabase(database.name, connectionSession: session) handle.succeed() - viewModel.setDatabaseLoading(connectionID: connID, databaseName: database.name, loading: false) } } label: { Label("Refresh", systemImage: "arrow.clockwise") diff --git a/Echo/Sources/Features/ObjectBrowser/Views/SearchSidebar/SearchSidebarView+Actions.swift b/Echo/Sources/Features/ObjectBrowser/Views/SearchSidebar/SearchSidebarView+Actions.swift index 4c8eddcc5..3fa9b3e00 100644 --- a/Echo/Sources/Features/ObjectBrowser/Views/SearchSidebar/SearchSidebarView+Actions.swift +++ b/Echo/Sources/Features/ObjectBrowser/Views/SearchSidebar/SearchSidebarView+Actions.swift @@ -12,15 +12,10 @@ extension SearchSidebarView { switch payload { case .schemaObject(let schema, let name, let type): - switch type { - case .table: - if openInNewTab { - openQueryPreview(forTable: name, schema: schema, session: session, database: databaseName) - } else { - focusExplorer(on: session, database: databaseName, schema: schema, objectName: name, columnName: nil, objectType: .table) - } - case .view, .materializedView, .function, .procedure, .trigger, .extension, .sequence, .type, .synonym: - openDefinition(for: name, schema: schema, type: type, in: session, database: databaseName) + if openInNewTab, type == .table { + openQueryPreview(forTable: name, schema: schema, session: session, database: databaseName) + } else { + focusExplorer(on: session, database: databaseName, schema: schema, objectName: name, columnName: nil, objectType: type) } case .column(let schema, let table, let column): @@ -45,12 +40,12 @@ extension SearchSidebarView { } case .function(let schema, let name): - openDefinition(for: name, schema: schema, type: .function, in: session, database: databaseName) + focusExplorer(on: session, database: databaseName, schema: schema, objectName: name, columnName: nil, objectType: .function) case .procedure(let schema, let name): - openDefinition(for: name, schema: schema, type: .procedure, in: session, database: databaseName) + focusExplorer(on: session, database: databaseName, schema: schema, objectName: name, columnName: nil, objectType: .procedure) case .trigger(let schema, _, let name): - openDefinition(for: name, schema: schema, type: .trigger, in: session, database: databaseName) + focusExplorer(on: session, database: databaseName, schema: schema, objectName: name, columnName: nil, objectType: .trigger) case .queryTab(let tabID, let connectionSessionID): environmentState.sessionGroup.setActiveSession(connectionSessionID) diff --git a/Echo/Sources/Features/Preferences/Domain/GlobalSettings.swift b/Echo/Sources/Features/Preferences/Domain/GlobalSettings.swift index d60d1d006..8617911b0 100644 --- a/Echo/Sources/Features/Preferences/Domain/GlobalSettings.swift +++ b/Echo/Sources/Features/Preferences/Domain/GlobalSettings.swift @@ -16,18 +16,6 @@ enum AccentColorSource: String, Codable, Hashable, CaseIterable { } } -enum NativePsqlRuntimePreference: String, Codable, Hashable, CaseIterable { - case bundled - case system - - var displayName: String { - switch self { - case .bundled: return "Bundled Binary" - case .system: return "System Binary" - } - } -} - enum SidebarAutoExpandSection: String, Codable, Hashable, CaseIterable, Identifiable { case databases case tables @@ -148,6 +136,7 @@ struct GlobalSettings: Codable, Hashable { var resultSpoolMaxBytes: Int = 5 * 1_024 * 1_024 * 1_024 var resultSpoolRetentionHours: Int = 72 var resultSpoolCustomLocation: String? + var objectBrowserCacheMaxBytes: Int = 512 * 1_024 * 1_024 var autoOpenInspectorOnSelection: Bool = true var autoOpenBottomPanel: Bool = true var diagramPrefetchMode: DiagramPrefetchMode = .off @@ -163,11 +152,6 @@ struct GlobalSettings: Codable, Hashable { var sidebarAutoExpandSQLServer: Set? var sidebarAutoExpandMySQL: Set? var managedPostgresConsoleEnabled: Bool = true - var nativePsqlEnabled: Bool = false - var nativePsqlRuntimePreference: NativePsqlRuntimePreference = .bundled - var nativePsqlAllowSystemBinaryFallback: Bool = false - var nativePsqlAllowShellEscape: Bool = true - var nativePsqlAllowFileCommands: Bool = true var pgToolCustomPath: String? var mysqlToolCustomPath: String? var sidebarIconColorMode: SidebarIconColorMode = .colorful @@ -233,6 +217,7 @@ struct GlobalSettings: Codable, Hashable { case showForeignKeysInInspector, showJsonInInspector case resultsInitialRowLimit case resultSpoolMaxBytes, resultSpoolRetentionHours, resultSpoolCustomLocation + case objectBrowserCacheMaxBytes case autoOpenInspectorOnSelection, autoOpenBottomPanel case diagramPrefetchMode, diagramRefreshCadence, diagramCacheMaxBytes case diagramVerifyBeforeRefresh, diagramRenderRelationshipsForLargeDiagrams, diagramUseThemedAppearance @@ -240,11 +225,6 @@ struct GlobalSettings: Codable, Hashable { case sidebarAutoExpandSections, sidebarCustomizePerDatabaseType case sidebarAutoExpandPostgresql, sidebarAutoExpandSQLServer, sidebarAutoExpandMySQL case managedPostgresConsoleEnabled - case nativePsqlEnabled - case nativePsqlRuntimePreference - case nativePsqlAllowSystemBinaryFallback - case nativePsqlAllowShellEscape - case nativePsqlAllowFileCommands case pgToolCustomPath case mysqlToolCustomPath case sidebarIconColorMode @@ -300,6 +280,7 @@ struct GlobalSettings: Codable, Hashable { resultSpoolMaxBytes = try container.decodeIfPresent(Int.self, forKey: .resultSpoolMaxBytes) ?? 5 * 1_024 * 1_024 * 1_024 resultSpoolRetentionHours = try container.decodeIfPresent(Int.self, forKey: .resultSpoolRetentionHours) ?? 72 resultSpoolCustomLocation = try container.decodeIfPresent(String.self, forKey: .resultSpoolCustomLocation) + objectBrowserCacheMaxBytes = max(64 * 1_024 * 1_024, try container.decodeIfPresent(Int.self, forKey: .objectBrowserCacheMaxBytes) ?? 512 * 1_024 * 1_024) autoOpenInspectorOnSelection = try container.decodeIfPresent(Bool.self, forKey: .autoOpenInspectorOnSelection) ?? true autoOpenBottomPanel = try container.decodeIfPresent(Bool.self, forKey: .autoOpenBottomPanel) ?? true diagramPrefetchMode = try container.decodeIfPresent(DiagramPrefetchMode.self, forKey: .diagramPrefetchMode) ?? .off @@ -315,11 +296,6 @@ struct GlobalSettings: Codable, Hashable { sidebarAutoExpandSQLServer = try container.decodeIfPresent(Set.self, forKey: .sidebarAutoExpandSQLServer) sidebarAutoExpandMySQL = try container.decodeIfPresent(Set.self, forKey: .sidebarAutoExpandMySQL) managedPostgresConsoleEnabled = try container.decodeIfPresent(Bool.self, forKey: .managedPostgresConsoleEnabled) ?? true - nativePsqlEnabled = try container.decodeIfPresent(Bool.self, forKey: .nativePsqlEnabled) ?? false - nativePsqlRuntimePreference = try container.decodeIfPresent(NativePsqlRuntimePreference.self, forKey: .nativePsqlRuntimePreference) ?? .bundled - nativePsqlAllowSystemBinaryFallback = try container.decodeIfPresent(Bool.self, forKey: .nativePsqlAllowSystemBinaryFallback) ?? false - nativePsqlAllowShellEscape = try container.decodeIfPresent(Bool.self, forKey: .nativePsqlAllowShellEscape) ?? true - nativePsqlAllowFileCommands = try container.decodeIfPresent(Bool.self, forKey: .nativePsqlAllowFileCommands) ?? true pgToolCustomPath = try container.decodeIfPresent(String.self, forKey: .pgToolCustomPath) mysqlToolCustomPath = try container.decodeIfPresent(String.self, forKey: .mysqlToolCustomPath) @@ -381,6 +357,7 @@ struct GlobalSettings: Codable, Hashable { try container.encode(resultSpoolMaxBytes, forKey: .resultSpoolMaxBytes) try container.encode(resultSpoolRetentionHours, forKey: .resultSpoolRetentionHours) try container.encodeIfPresent(resultSpoolCustomLocation, forKey: .resultSpoolCustomLocation) + try container.encode(objectBrowserCacheMaxBytes, forKey: .objectBrowserCacheMaxBytes) try container.encode(autoOpenInspectorOnSelection, forKey: .autoOpenInspectorOnSelection) try container.encode(autoOpenBottomPanel, forKey: .autoOpenBottomPanel) try container.encode(diagramPrefetchMode, forKey: .diagramPrefetchMode) @@ -399,11 +376,6 @@ struct GlobalSettings: Codable, Hashable { try container.encodeIfPresent(sidebarAutoExpandSQLServer, forKey: .sidebarAutoExpandSQLServer) try container.encodeIfPresent(sidebarAutoExpandMySQL, forKey: .sidebarAutoExpandMySQL) try container.encode(managedPostgresConsoleEnabled, forKey: .managedPostgresConsoleEnabled) - try container.encode(nativePsqlEnabled, forKey: .nativePsqlEnabled) - try container.encode(nativePsqlRuntimePreference, forKey: .nativePsqlRuntimePreference) - try container.encode(nativePsqlAllowSystemBinaryFallback, forKey: .nativePsqlAllowSystemBinaryFallback) - try container.encode(nativePsqlAllowShellEscape, forKey: .nativePsqlAllowShellEscape) - try container.encode(nativePsqlAllowFileCommands, forKey: .nativePsqlAllowFileCommands) try container.encodeIfPresent(pgToolCustomPath, forKey: .pgToolCustomPath) try container.encodeIfPresent(mysqlToolCustomPath, forKey: .mysqlToolCustomPath) try container.encode(sidebarIconColorMode, forKey: .sidebarIconColorMode) diff --git a/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Actions.swift b/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Actions.swift index 2ee426cd8..19faba6d9 100644 --- a/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Actions.swift +++ b/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Actions.swift @@ -67,4 +67,25 @@ extension ApplicationCacheSettingsView { await refreshDiagramCacheUsage() } } + + func refreshObjectBrowserCacheUsage() async { + let shouldContinue = await MainActor.run { () -> Bool in + if isRefreshingObjectBrowserCache { return false } + isRefreshingObjectBrowserCache = true + return true + } + guard shouldContinue else { return } + let usage = await environmentState.objectBrowserCacheUsageBytes() + await MainActor.run { + objectBrowserCacheUsage = usage + isRefreshingObjectBrowserCache = false + } + } + + func clearObjectBrowserCache() { + Task { + await environmentState.clearObjectBrowserCache() + await refreshObjectBrowserCacheUsage() + } + } } diff --git a/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Bindings.swift b/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Bindings.swift index 784de94c4..5d304e6e9 100644 --- a/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Bindings.swift +++ b/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Bindings.swift @@ -26,6 +26,21 @@ extension ApplicationCacheSettingsView { ) } + var objectBrowserCacheMaxBinding: Binding { + Binding( + get: { projectStore.globalSettings.objectBrowserCacheMaxBytes }, + set: { newValue in + var settings = projectStore.globalSettings + settings.objectBrowserCacheMaxBytes = max(64 * 1_024 * 1_024, newValue) + Task { + try? await projectStore.updateGlobalSettings(settings) + await environmentState.objectBrowserCacheStore.pruneToLimit(settings.objectBrowserCacheMaxBytes) + await refreshObjectBrowserCacheUsage() + } + } + ) + } + var clipboardEnabledBinding: Binding { Binding( get: { clipboardHistory.isEnabled }, diff --git a/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Sections.swift b/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Sections.swift index 8b2c41e4c..607655dbd 100644 --- a/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Sections.swift +++ b/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView+Sections.swift @@ -14,6 +14,17 @@ extension ApplicationCacheSettingsView { .pickerStyle(.menu) .frame(minWidth: 120, idealWidth: 160, maxWidth: 200, alignment: .trailing) } + + PropertyRow(title: "Object browser cache") { + Picker("", selection: objectBrowserCacheMaxBinding) { + ForEach(Self.perTypeStorageOptions, id: \.bytes) { option in + Text(option.label).tag(option.bytes) + } + } + .labelsHidden() + .pickerStyle(.menu) + .frame(minWidth: 120, idealWidth: 160, maxWidth: 200, alignment: .trailing) + } } } } diff --git a/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView.swift b/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView.swift index a13fbe158..f620820e2 100644 --- a/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView.swift +++ b/Echo/Sources/Features/Preferences/Views/ApplicationCacheSettings/ApplicationCacheSettingsView.swift @@ -18,6 +18,8 @@ struct ApplicationCacheSettingsView: View { @State var isRefreshingAutocompleteHistory = false @State var diagramCacheUsage: UInt64 = 0 @State var isRefreshingDiagramCache = false + @State var objectBrowserCacheUsage: UInt64 = 0 + @State var isRefreshingObjectBrowserCache = false var body: some View { Form { @@ -32,6 +34,7 @@ struct ApplicationCacheSettingsView: View { await refreshResultCacheUsage() await refreshAutocompleteHistoryUsage() await refreshDiagramCacheUsage() + await refreshObjectBrowserCacheUsage() } .alert("Disable Clipboard History?", isPresented: $confirmDisableHistory) { Button("Disable", role: .destructive) { @@ -79,6 +82,14 @@ struct ApplicationCacheSettingsView: View { onClear: { clearResultCache() } ) + storageUsageRow( + title: "Object Browser Cache", + usage: objectBrowserCacheUsage, + isRefreshing: isRefreshingObjectBrowserCache, + onRefresh: { await refreshObjectBrowserCacheUsage() }, + onClear: { clearObjectBrowserCache() } + ) + storageUsageRow( title: "Diagram Cache", usage: diagramCacheUsage, diff --git a/Echo/Sources/Features/Preferences/Views/DatabasesSettings/DatabasesSettingsView+Bindings.swift b/Echo/Sources/Features/Preferences/Views/DatabasesSettings/DatabasesSettingsView+Bindings.swift index f67cf3b3f..a38950b66 100644 --- a/Echo/Sources/Features/Preferences/Views/DatabasesSettings/DatabasesSettingsView+Bindings.swift +++ b/Echo/Sources/Features/Preferences/Views/DatabasesSettings/DatabasesSettingsView+Bindings.swift @@ -24,26 +24,6 @@ extension DatabasesSettingsView { binding(for: \.managedPostgresConsoleEnabled) } - var nativePsqlBinding: Binding { - binding(for: \.nativePsqlEnabled) - } - - var runtimePreferenceBinding: Binding { - binding(for: \.nativePsqlRuntimePreference) - } - - var systemFallbackBinding: Binding { - binding(for: \.nativePsqlAllowSystemBinaryFallback) - } - - var shellEscapeBinding: Binding { - binding(for: \.nativePsqlAllowShellEscape) - } - - var fileCommandsBinding: Binding { - binding(for: \.nativePsqlAllowFileCommands) - } - var pgToolCustomPathBinding: Binding { Binding( get: { settings.pgToolCustomPath ?? "" }, diff --git a/Echo/Sources/Features/Preferences/Views/DatabasesSettings/DatabasesSettingsView+PostgresSettings.swift b/Echo/Sources/Features/Preferences/Views/DatabasesSettings/DatabasesSettingsView+PostgresSettings.swift index 385702321..9b28f11c2 100644 --- a/Echo/Sources/Features/Preferences/Views/DatabasesSettings/DatabasesSettingsView+PostgresSettings.swift +++ b/Echo/Sources/Features/Preferences/Views/DatabasesSettings/DatabasesSettingsView+PostgresSettings.swift @@ -14,13 +14,13 @@ extension DatabasesSettingsView { return "pg_dump not found" } - /// PostgreSQL-specific settings: managed console, native psql policy, restrictions. + /// PostgreSQL-specific settings: managed console, backup tools. @ViewBuilder var postgresSettings: some View { Section("Managed Console") { PropertyRow( title: "Enable Postgres Console", - info: "The Postgres Console is Echo's managed PostgreSQL console powered by the app's Postgres engine. Use this for the current PostgreSQL console inside Echo. Native psql is configured separately." + info: "The Postgres Console is Echo's managed PostgreSQL console powered by the app's Postgres engine." ) { Toggle("", isOn: managedConsoleBinding) .labelsHidden() @@ -28,39 +28,6 @@ extension DatabasesSettingsView { } } - Section("Native psql") { - PropertyRow( - title: "Enable Native psql", - info: "Expose the future native psql entry point in Echo. This currently controls policy and UI availability only. Native psql is intended for exact CLI compatibility." - ) { - Toggle("", isOn: nativePsqlBinding) - .labelsHidden() - .toggleStyle(.switch) - } - - PropertyRow(title: "Runtime Preference") { - Picker("", selection: runtimePreferenceBinding) { - ForEach(NativePsqlRuntimePreference.allCases, id: \.self) { preference in - Text(preference.displayName) - .tag(preference) - } - } - .labelsHidden() - .pickerStyle(.menu) - } - .disabled(!settings.nativePsqlEnabled) - - PropertyRow( - title: "Allow System Binary Fallback", - info: "If Echo cannot use its preferred psql runtime, allow falling back to a system-installed psql binary." - ) { - Toggle("", isOn: systemFallbackBinding) - .labelsHidden() - .toggleStyle(.switch) - } - .disabled(!settings.nativePsqlEnabled) - } - Section("Backup & Restore Tools") { PropertyRow( title: "Tool Path", @@ -93,26 +60,5 @@ extension DatabasesSettingsView { } } - Section("Restrictions") { - PropertyRow( - title: "Allow Shell Escape", - info: "Controls whether a future native psql session should permit shell escape commands such as \\!. These toggles establish the policy model now so the app can grow without redesigning database settings later." - ) { - Toggle("", isOn: shellEscapeBinding) - .labelsHidden() - .toggleStyle(.switch) - } - .disabled(!settings.nativePsqlEnabled) - - PropertyRow( - title: "Allow File Commands", - info: "Controls whether a future native psql session should permit filesystem-driven commands such as \\i and copy workflows." - ) { - Toggle("", isOn: fileCommandsBinding) - .labelsHidden() - .toggleStyle(.switch) - } - .disabled(!settings.nativePsqlEnabled) - } } } diff --git a/Echo/Sources/Features/Preferences/Views/NotificationSettingsView+Bindings.swift b/Echo/Sources/Features/Preferences/Views/NotificationSettingsView+Bindings.swift new file mode 100644 index 000000000..655dea0d4 --- /dev/null +++ b/Echo/Sources/Features/Preferences/Views/NotificationSettingsView+Bindings.swift @@ -0,0 +1,64 @@ +import SwiftUI + +extension NotificationSettingsView { + var hasAnyEnabledNotifications: Bool { + NotificationCategory.allCases.contains { preferences.isEnabled($0) } + } + + var allEnabledBinding: Binding { + Binding( + get: { hasAnyEnabledNotifications }, + set: { enabled in + var updated = preferences + if enabled { + updated.enableAll() + } else { + updated.disableAll() + } + save(updated) + } + ) + } + + var deliveryBinding: Binding { + Binding( + get: { preferences.delivery }, + set: { newValue in + var updated = preferences + updated.delivery = newValue + save(updated) + } + ) + } + + func groupBinding(for group: NotificationGroup) -> Binding { + Binding( + get: { preferences.isGroupEnabled(group) }, + set: { enabled in + var updated = preferences + updated.setGroupEnabled(enabled, for: group) + save(updated) + } + ) + } + + func categoryBinding(for category: NotificationCategory) -> Binding { + Binding( + get: { preferences.isEnabled(category) }, + set: { enabled in + var updated = preferences + updated.markExplicitPreferences() + updated.setEnabled(enabled, for: category) + save(updated) + } + ) + } + + func save(_ preferences: NotificationPreferences) { + var settings = projectStore.globalSettings + settings.notificationPreferences = preferences + Task { + try? await projectStore.updateGlobalSettings(settings) + } + } +} diff --git a/Echo/Sources/Features/Preferences/Views/NotificationSettingsView+Components.swift b/Echo/Sources/Features/Preferences/Views/NotificationSettingsView+Components.swift new file mode 100644 index 000000000..4d8093add --- /dev/null +++ b/Echo/Sources/Features/Preferences/Views/NotificationSettingsView+Components.swift @@ -0,0 +1,96 @@ +import SwiftUI +import AppKit + +extension NotificationSettingsView { + var detailContent: some View { + Form { + overviewSection + deliverySection + ForEach(NotificationGroup.allCases) { group in + categorySection(for: group) + } + } + .formStyle(.grouped) + .scrollContentBackground(.hidden) + } + + var overviewSection: some View { + Section { + HStack(alignment: .center, spacing: SpacingTokens.md) { + Image(nsImage: NSApplication.shared.applicationIconImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 64, height: 64) + .clipShape(RoundedRectangle(cornerRadius: 14)) + + VStack(alignment: .leading, spacing: SpacingTokens.xxxs) { + Text("Echo Notifications") + .font(TypographyTokens.title3.weight(.semibold)) + + Text("Control how Echo appears in Notification Center and inside the app.") + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Text.secondary) + } + + Spacer() + } + + PropertyRow( + title: "Allow notifications", + subtitle: "Turn all Echo notifications on or off." + ) { + Toggle("", isOn: allEnabledBinding) + .labelsHidden() + .toggleStyle(.switch) + } + } footer: { + Text("Echo follows these preferences for both in-app toasts and native macOS notifications.") + } + } + + var deliverySection: some View { + Section("Delivery") { + PropertyRow( + title: "Notification delivery", + subtitle: preferences.delivery.displayDescription, + info: "Choose whether Echo shows banners inside the app, through macOS Notification Center, or both." + ) { + Picker("", selection: deliveryBinding) { + ForEach(NotificationDelivery.allCases, id: \.self) { method in + Text(method.displayName) + .tag(method) + } + } + .labelsHidden() + .pickerStyle(.menu) + .disabled(!allEnabledBinding.wrappedValue) + } + } + } + + func categorySection(for group: NotificationGroup) -> some View { + Section(group.displayName) { + PropertyRow( + title: "Allow \(group.displayName.lowercased()) notifications", + subtitle: group.displayDescription + ) { + Toggle("", isOn: groupBinding(for: group)) + .labelsHidden() + .toggleStyle(.switch) + .disabled(!allEnabledBinding.wrappedValue) + } + + ForEach(group.categories) { category in + PropertyRow( + title: category.displayName, + subtitle: category.displayDescription + ) { + Toggle("", isOn: categoryBinding(for: category)) + .labelsHidden() + .toggleStyle(.switch) + .disabled(!allEnabledBinding.wrappedValue) + } + } + } + } +} diff --git a/Echo/Sources/Features/Preferences/Views/NotificationSettingsView.swift b/Echo/Sources/Features/Preferences/Views/NotificationSettingsView.swift index 506474b30..29b593a5c 100644 --- a/Echo/Sources/Features/Preferences/Views/NotificationSettingsView.swift +++ b/Echo/Sources/Features/Preferences/Views/NotificationSettingsView.swift @@ -1,76 +1,14 @@ import SwiftUI +import AppKit struct NotificationSettingsView: View { - @Environment(ProjectStore.self) private var projectStore + @Environment(ProjectStore.self) internal var projectStore - private var preferences: NotificationPreferences { + internal var preferences: NotificationPreferences { projectStore.globalSettings.notificationPreferences } var body: some View { - Form { - deliverySection - categoriesSection - } - .formStyle(.grouped) - .scrollContentBackground(.hidden) - } - - // MARK: - Delivery - - private var deliverySection: some View { - Section("Delivery Method") { - PropertyRow(title: "Deliver notifications via") { - Picker("", selection: deliveryBinding) { - ForEach(NotificationDelivery.allCases, id: \.self) { method in - Text(method.displayName).tag(method) - } - } - .labelsHidden() - .pickerStyle(.menu) - } - } - } - - private var deliveryBinding: Binding { - Binding( - get: { preferences.delivery }, - set: { newValue in - var settings = projectStore.globalSettings - settings.notificationPreferences.delivery = newValue - Task { try? await projectStore.updateGlobalSettings(settings) } - } - ) - } - - // MARK: - Categories - - private var categoriesSection: some View { - Section("Notification Categories") { - ForEach(NotificationGroup.allCases) { group in - DisclosureGroup { - ForEach(group.categories) { category in - PropertyRow(title: category.displayName) { - Toggle("", isOn: categoryBinding(for: category)) - .labelsHidden() - .toggleStyle(.switch) - } - } - } label: { - Label(group.displayName, systemImage: group.systemImage) - } - } - } - } - - private func categoryBinding(for category: NotificationCategory) -> Binding { - Binding( - get: { preferences.isEnabled(category) }, - set: { enabled in - var settings = projectStore.globalSettings - settings.notificationPreferences.setEnabled(enabled, for: category) - Task { try? await projectStore.updateGlobalSettings(settings) } - } - ) + detailContent } } diff --git a/Echo/Sources/Features/Preferences/Views/QueryResultsSettings/ResultGridColorSettingsSection.swift b/Echo/Sources/Features/Preferences/Views/QueryResultsSettings/ResultGridColorSettingsSection.swift index 576a15f1c..8920b8f07 100644 --- a/Echo/Sources/Features/Preferences/Views/QueryResultsSettings/ResultGridColorSettingsSection.swift +++ b/Echo/Sources/Features/Preferences/Views/QueryResultsSettings/ResultGridColorSettingsSection.swift @@ -99,7 +99,7 @@ struct ResultGridColorSettingsSection: View { } .padding(SpacingTokens.xs) .background(ColorTokens.Background.secondary.opacity(0.5)) - .cornerRadius(6) + .clipShape(RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.small)) } } } diff --git a/Echo/Sources/Features/QueryBuilder/Views/QueryBuilderTableNode.swift b/Echo/Sources/Features/QueryBuilder/Views/QueryBuilderTableNode.swift index 94627e68f..dc884adc5 100644 --- a/Echo/Sources/Features/QueryBuilder/Views/QueryBuilderTableNode.swift +++ b/Echo/Sources/Features/QueryBuilder/Views/QueryBuilderTableNode.swift @@ -42,8 +42,9 @@ struct QueryBuilderTableNode: View { if column.isPrimaryKey { Image(systemName: "key.fill") - .font(.system(size: 8)) - .foregroundStyle(.yellow) + .font(TypographyTokens.compact) + .imageScale(.small) + .foregroundStyle(ColorTokens.Status.warning) } Text(column.name) @@ -63,16 +64,16 @@ struct QueryBuilderTableNode: View { .lineLimit(1) } .padding(.horizontal, SpacingTokens.xs) - .padding(.vertical, 2) + .padding(.vertical, SpacingTokens.xxs2) } } .padding(.vertical, SpacingTokens.xxs2) } .frame(width: 220) .background(ColorTokens.Background.primary) - .clipShape(RoundedRectangle(cornerRadius: 6)) + .clipShape(RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.small)) .overlay( - RoundedRectangle(cornerRadius: 6) + RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.small) .stroke(ColorTokens.Separator.secondary, lineWidth: 1) ) .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2) diff --git a/Echo/Sources/Features/QueryWorkspace/Domain/PSQL/PSQLTabViewModel+MetaCommands.swift b/Echo/Sources/Features/QueryWorkspace/Domain/PSQL/PSQLTabViewModel+MetaCommands.swift index 6103c744e..631d9875c 100644 --- a/Echo/Sources/Features/QueryWorkspace/Domain/PSQL/PSQLTabViewModel+MetaCommands.swift +++ b/Echo/Sources/Features/QueryWorkspace/Domain/PSQL/PSQLTabViewModel+MetaCommands.swift @@ -320,9 +320,8 @@ extension PSQLTabViewModel { \\du list roles \\x toggle expanded output - Not supported in the managed console: - shell/file/client-local commands such as \\!, \\i, \\ir, \\o, \\w, \\copy, \\cd, \\set, \\watch - These belong to native psql, which is intended for exact CLI compatibility. + Not available in the managed console (use "Open in psql" for these): + \\!, \\i, \\ir, \\o, \\w, \\copy, \\cd, \\set, \\watch """ } @@ -334,7 +333,7 @@ extension PSQLTabViewModel { func unsupportedCommandMessage(for command: String) -> String { let nativeOnlyCommands: Set = ["\\!", "\\i", "\\ir", "\\o", "\\w", "\\copy", "\\cd", "\\set", "\\watch", "\\prompt", "\\password"] if nativeOnlyCommands.contains(command) { - return "ERROR: \(command) is a native psql client command and is not supported in Echo's managed Postgres Console.\n" + return "ERROR: \(command) requires the native psql CLI. Use \"Open in psql\" from the database context menu.\n" } return "ERROR: Unsupported psql command: \(command)\n" } diff --git a/Echo/Sources/Features/QueryWorkspace/Domain/PSQL/PSQLTabViewModel.swift b/Echo/Sources/Features/QueryWorkspace/Domain/PSQL/PSQLTabViewModel.swift index 6589b2737..008d1040e 100644 --- a/Echo/Sources/Features/QueryWorkspace/Domain/PSQL/PSQLTabViewModel.swift +++ b/Echo/Sources/Features/QueryWorkspace/Domain/PSQL/PSQLTabViewModel.swift @@ -41,8 +41,8 @@ final class PSQLTabViewModel: Identifiable { let version = connection.serverVersion ?? "unknown" history = "Postgres Console (Echo), server \(version)\n" - history += "This is Echo's managed PostgreSQL console powered by a dedicated connection.\n" - history += "Native psql is a separate feature and is not wired into this build yet.\n\n" + history += "Type SQL or use backslash commands (\\? for help).\n" + history += "For the native psql CLI, right-click a database and choose \"Open in psql\".\n\n" prompt() Task { await resolveActiveDatabase() diff --git a/Echo/Sources/Features/QueryWorkspace/Domain/QueryEditorState/QueryEditorState.swift b/Echo/Sources/Features/QueryWorkspace/Domain/QueryEditorState/QueryEditorState.swift index 8d58868ef..4a21e758a 100644 --- a/Echo/Sources/Features/QueryWorkspace/Domain/QueryEditorState/QueryEditorState.swift +++ b/Echo/Sources/Features/QueryWorkspace/Domain/QueryEditorState/QueryEditorState.swift @@ -21,6 +21,7 @@ import OSLog var currentExecutionTime: TimeInterval = 0 var rowProgress: RowProgress = RowProgress() var messages: [QueryExecutionMessage] = [] + var prefersMessagesAfterExecution: Bool = false var hasExecutedAtLeastOnce: Bool = false var splitRatio: CGFloat = 0.5 var wasCancelled: Bool = false diff --git a/Echo/Sources/Features/QueryWorkspace/Domain/TableStructureEditor/TableStructureEditorViewModel+Logic.swift b/Echo/Sources/Features/QueryWorkspace/Domain/TableStructureEditor/TableStructureEditorViewModel+Logic.swift index 85b2d29e9..d1ed33f9f 100644 --- a/Echo/Sources/Features/QueryWorkspace/Domain/TableStructureEditor/TableStructureEditorViewModel+Logic.swift +++ b/Echo/Sources/Features/QueryWorkspace/Domain/TableStructureEditor/TableStructureEditorViewModel+Logic.swift @@ -1,7 +1,7 @@ import Foundation extension TableStructureEditorViewModel { - + func reset(to details: TableStructureDetails) { apply(details: details) } @@ -48,19 +48,20 @@ extension TableStructureEditorViewModel { let columns = index.columns.map { column in IndexModel.Column(name: column.name, sortOrder: column.sortOrder == .descending ? .descending : .ascending, isIncluded: column.isIncluded) } + let resolvedIndexType = resolvedIndexType(for: index.indexType) return IndexModel( original: IndexModel.Snapshot( name: index.name, columns: columns.map { $0.snapshot }, isUnique: index.isUnique, filterCondition: index.filterCondition, - indexType: index.indexType + indexType: resolvedIndexType ), name: index.name, columns: columns, isUnique: index.isUnique, filterCondition: index.filterCondition ?? "", - indexType: index.indexType ?? (databaseType == .microsoftSQL ? "nonclustered" : "btree") + indexType: resolvedIndexType ) } @@ -137,6 +138,14 @@ extension TableStructureEditorViewModel { tableProperties = details.tableProperties } + private func resolvedIndexType(for indexType: String?) -> String { + if let trimmed = indexType?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty { + return trimmed + } + + return databaseType == .microsoftSQL ? "nonclustered" : "btree" + } + internal func generateStatements() -> [String] { let dialect = dialectGenerator let qualifiedTable = dialect.qualifiedTable(schema: schemaName, table: tableName) diff --git a/Echo/Sources/Features/QueryWorkspace/Domain/TableStructureEditor/TableStructureEditorViewModel.swift b/Echo/Sources/Features/QueryWorkspace/Domain/TableStructureEditor/TableStructureEditorViewModel.swift index 74c975421..f360bafc6 100644 --- a/Echo/Sources/Features/QueryWorkspace/Domain/TableStructureEditor/TableStructureEditorViewModel.swift +++ b/Echo/Sources/Features/QueryWorkspace/Domain/TableStructureEditor/TableStructureEditorViewModel.swift @@ -41,6 +41,14 @@ enum TableStructureSection: String, CaseIterable, Identifiable { @MainActor @Observable final class TableStructureEditorViewModel { + enum PendingAddAction: Equatable { + case column + case index + case foreignKey + case uniqueConstraint + case checkConstraint + } + var columns: [ColumnModel] = [] var indexes: [IndexModel] = [] var uniqueConstraints: [UniqueConstraintModel] = [] @@ -50,11 +58,13 @@ final class TableStructureEditorViewModel { var primaryKey: PrimaryKeyModel? var tableProperties: TableStructureDetails.TableProperties? var requestedSection: TableStructureSection? + var pendingAddAction: PendingAddAction? var isLoading: Bool = false var isApplying: Bool = false var lastError: String? var lastSuccessMessage: String? + private var isInitialized: Bool = false /// nil = not yet checked, true = has data, false = no data var partitionsAvailable: Bool? @@ -69,6 +79,7 @@ final class TableStructureEditorViewModel { internal var removedPrimaryKeyName: String? @ObservationIgnored var activityEngine: ActivityEngine? @ObservationIgnored var connectionSessionID: UUID? + @ObservationIgnored let sheetCoordinator = TableStructureSheetCoordinator() /// Update the session to point at a different database connection. /// Always triggers a reload since the previous session may have returned empty results. @@ -98,6 +109,8 @@ final class TableStructureEditorViewModel { // so the view shows a spinner immediately instead of an empty state flash. if details.columns.isEmpty { isLoading = true + } else { + isInitialized = true } } @@ -105,12 +118,18 @@ final class TableStructureEditorViewModel { requestedSection = section } + func requestAddAction(_ action: PendingAddAction, section: TableStructureSection) { + requestedSection = section + pendingAddAction = action + } + func reload() async { isLoading = true defer { isLoading = false } do { let details = try await session.getTableStructureDetails(schema: schemaName, table: tableName) apply(details: details) + isInitialized = true } catch { lastError = error.localizedDescription } @@ -124,23 +143,22 @@ final class TableStructureEditorViewModel { isApplying = true defer { isApplying = false } let handle = activityEngine?.begin("Alter \(tableName)", connectionSessionID: connectionSessionID) - let dialect = dialectGenerator do { - for statement in [dialect.beginTransaction()] + statements + [dialect.commitTransaction()] { - _ = try await session.executeUpdate(statement) - } + try await session.executeUpdatesAtomically(statements) let refreshed = try await session.getTableStructureDetails(schema: schemaName, table: tableName) apply(details: refreshed) lastSuccessMessage = "Structure updated" handle?.succeed() } catch { - _ = try? await session.executeUpdate(dialect.rollbackTransaction()) lastError = error.localizedDescription handle?.fail(error.localizedDescription) } } var hasPendingChanges: Bool { + // Don't report pending changes during initial load or reload + guard !isLoading && isInitialized else { return false } + if columns.contains(where: { $0.isDirty }) { return true } if indexes.contains(where: { $0.isDirty }) { return true } if uniqueConstraints.contains(where: { $0.isDirty }) { return true } diff --git a/Echo/Sources/Features/QueryWorkspace/Formatting/SQLFormatter.swift b/Echo/Sources/Features/QueryWorkspace/Formatting/SQLFormatter.swift index b815674a0..6a03859f6 100644 --- a/Echo/Sources/Features/QueryWorkspace/Formatting/SQLFormatter.swift +++ b/Echo/Sources/Features/QueryWorkspace/Formatting/SQLFormatter.swift @@ -102,10 +102,18 @@ final class SQLFormatter: SQLFormatterProtocol, Sendable { throw SQLFormatterError.formattingFailed(message) } + if result?.isUndefined == true || result?.isNull == true { + return sql + } + guard let formatted = result?.toString(), !formatted.isEmpty else { return sql } + if formatted == "undefined" || formatted == "null" { + return sql + } + return formatted } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/ExtensionStructure/PostgresExtensionStructureView.swift b/Echo/Sources/Features/QueryWorkspace/Views/ExtensionStructure/PostgresExtensionStructureView.swift index 02c3b6b82..315b20780 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/ExtensionStructure/PostgresExtensionStructureView.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/ExtensionStructure/PostgresExtensionStructureView.swift @@ -99,11 +99,11 @@ struct PostgresExtensionStructureView: View { ProgressView().controlSize(.small) } else { Label("Update to v\(viewModel.latestVersion ?? "?")", systemImage: "arrow.up.circle.fill") - .foregroundStyle(Color.white) + .foregroundStyle(.white) .padding(.horizontal, SpacingTokens.xs) .padding(.vertical, SpacingTokens.xxs) .background(ColorTokens.Status.info) - .cornerRadius(6) + .clipShape(RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.small)) } } .buttonStyle(.plain) diff --git a/Echo/Sources/Features/QueryWorkspace/Views/Results/Section/QueryResultsSection+Logic.swift b/Echo/Sources/Features/QueryWorkspace/Views/Results/Section/QueryResultsSection+Logic.swift index 19c02bf75..beb1cbb8d 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/Results/Section/QueryResultsSection+Logic.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/Results/Section/QueryResultsSection+Logic.swift @@ -1,6 +1,10 @@ import SwiftUI extension QueryResultsSection { + private var prefersMessagesAfterExecution: Bool { + guard query.errorMessage == nil, !query.isExecuting else { return false } + return query.prefersMessagesAfterExecution + } internal func handleResultTokenChange() { let newIDs = tableColumns.map(\.id) @@ -9,8 +13,10 @@ extension QueryResultsSection { sortCriteria = nil highlightedColumnIndex = nil } - if panelState.selectedSegment == .messages, query.errorMessage == nil, - !(connection.databaseType == .microsoftSQL && query.statisticsEnabled) { + if prefersMessagesAfterExecution { + panelState.selectedSegment = .messages + if !panelState.isOpen { panelState.isOpen = true } + } else if panelState.selectedSegment == .messages, query.errorMessage == nil { panelState.selectedSegment = .results } rebuildRowOrder() @@ -25,11 +31,7 @@ extension QueryResultsSection { panelState.selectedSegment = .results } } else { - // When statistics mode is on, auto-switch to Messages so the user - // sees the IO/TIME output without having to click manually. - if connection.databaseType == .microsoftSQL, - query.statisticsEnabled, - query.errorMessage == nil { + if prefersMessagesAfterExecution { panelState.selectedSegment = .messages if !panelState.isOpen { panelState.isOpen = true } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/Results/Spatial/SpatialBrowserLinkBuilder.swift b/Echo/Sources/Features/QueryWorkspace/Views/Results/Spatial/SpatialBrowserLinkBuilder.swift index b5a128e86..dacaa530e 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/Results/Spatial/SpatialBrowserLinkBuilder.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/Results/Spatial/SpatialBrowserLinkBuilder.swift @@ -61,7 +61,11 @@ enum SpatialBrowserLinkBuilder { private static func browserCoordinateString(_ value: Double) -> String { let roundedValue = (value * 1_000_000).rounded() / 1_000_000 - return roundedValue.formatted(.number.precision(.fractionLength(0...6))) + return roundedValue.formatted( + .number + .precision(.fractionLength(0...6)) + .locale(Locale(identifier: "en_US_POSIX")) + ) } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/CheckConstraintEditorSheet+Draft.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/CheckConstraintEditorSheet+Draft.swift index 180c7577c..a6ef91174 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/CheckConstraintEditorSheet+Draft.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/CheckConstraintEditorSheet+Draft.swift @@ -3,8 +3,17 @@ import Foundation extension CheckConstraintEditorSheet { func applyDraftChanges() { - constraint.name = draft.name.trimmingCharacters(in: .whitespacesAndNewlines) - constraint.expression = draft.expression.trimmingCharacters(in: .whitespacesAndNewlines) + let updatedConstraint = TableStructureEditorViewModel.CheckConstraintModel( + original: constraint.original, + name: draft.name.trimmingCharacters(in: .whitespacesAndNewlines), + expression: draft.expression.trimmingCharacters(in: .whitespacesAndNewlines) + ) + + if draft.isEditingExisting { + constraint = updatedConstraint + } else { + onSaveNew?(updatedConstraint) + } } func cancelIfNew() { diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/CheckConstraintEditorSheet.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/CheckConstraintEditorSheet.swift index 7e1af25c7..2983adcb9 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/CheckConstraintEditorSheet.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/CheckConstraintEditorSheet.swift @@ -4,6 +4,7 @@ struct CheckConstraintEditorSheet: View { @Binding var constraint: TableStructureEditorViewModel.CheckConstraintModel let onDelete: () -> Void let onCancelNew: () -> Void + let onSaveNew: ((TableStructureEditorViewModel.CheckConstraintModel) -> Void)? @Environment(\.dismiss) private var dismiss @State var draft: Draft @@ -11,11 +12,13 @@ struct CheckConstraintEditorSheet: View { init( constraint: Binding, onDelete: @escaping () -> Void, - onCancelNew: @escaping () -> Void + onCancelNew: @escaping () -> Void, + onSaveNew: ((TableStructureEditorViewModel.CheckConstraintModel) -> Void)? = nil ) { self._constraint = constraint self.onDelete = onDelete self.onCancelNew = onCancelNew + self.onSaveNew = onSaveNew _draft = State(initialValue: Draft(model: constraint.wrappedValue)) } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ColumnEditorSheet+Draft.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ColumnEditorSheet+Draft.swift index 2401537be..f14837577 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ColumnEditorSheet+Draft.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ColumnEditorSheet+Draft.swift @@ -84,31 +84,39 @@ extension ColumnEditorSheet { } func applyDraft() { - column.name = draft.name.trimmingCharacters(in: .whitespacesAndNewlines) - column.dataType = draft.dataType.trimmingCharacters(in: .whitespacesAndNewlines) - column.isNullable = draft.isNullable - let defaultTrimmed = draft.defaultValue.trimmingCharacters(in: .whitespacesAndNewlines) - column.defaultValue = defaultTrimmed.isEmpty ? nil : defaultTrimmed - let expressionTrimmed = draft.generatedExpression.trimmingCharacters(in: .whitespacesAndNewlines) - column.generatedExpression = expressionTrimmed.isEmpty ? nil : expressionTrimmed - - column.isIdentity = draft.isIdentity - column.identitySeed = draft.isIdentity ? Int(draft.identitySeed) : nil - column.identityIncrement = draft.isIdentity ? Int(draft.identityIncrement) : nil - column.identityGeneration = draft.isIdentity ? draft.identityGeneration : nil - let collationTrimmed = draft.collation.trimmingCharacters(in: .whitespacesAndNewlines) - column.collation = collationTrimmed.isEmpty ? nil : collationTrimmed let characterSetTrimmed = draft.characterSet.trimmingCharacters(in: .whitespacesAndNewlines) - column.characterSet = characterSetTrimmed.isEmpty ? nil : characterSetTrimmed let commentTrimmed = draft.comment.trimmingCharacters(in: .whitespacesAndNewlines) - column.comment = commentTrimmed.isEmpty ? nil : commentTrimmed - column.isUnsigned = draft.isUnsigned - column.isZerofill = draft.isZerofill + let updatedColumn = TableStructureEditorViewModel.ColumnModel( + original: column.original, + name: draft.name.trimmingCharacters(in: .whitespacesAndNewlines), + dataType: draft.dataType.trimmingCharacters(in: .whitespacesAndNewlines), + isNullable: draft.isNullable, + defaultValue: defaultTrimmed.isEmpty ? nil : defaultTrimmed, + generatedExpression: expressionTrimmed.isEmpty ? nil : expressionTrimmed, + isIdentity: draft.isIdentity, + identitySeed: draft.isIdentity ? Int(draft.identitySeed) : nil, + identityIncrement: draft.isIdentity ? Int(draft.identityIncrement) : nil, + identityGeneration: draft.isIdentity ? draft.identityGeneration : nil, + collation: collationTrimmed.isEmpty ? nil : collationTrimmed, + characterSet: characterSetTrimmed.isEmpty ? nil : characterSetTrimmed, + comment: commentTrimmed.isEmpty ? nil : commentTrimmed, + isUnsigned: draft.isUnsigned, + isZerofill: draft.isZerofill, + ordinalPosition: column.ordinalPosition + ) - dismiss() + if draft.isEditingExisting { + column = updatedColumn + dismiss() + } else { + dismiss() + Task { @MainActor in + onSaveNew?(updatedColumn) + } + } } func cancelEditing() { diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ColumnEditorSheet.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ColumnEditorSheet.swift index 605c8d521..01fe10217 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ColumnEditorSheet.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ColumnEditorSheet.swift @@ -9,6 +9,7 @@ struct ColumnEditorSheet: View { let databaseType: DatabaseType let onDelete: () -> Void let onCancelNew: () -> Void + let onSaveNew: ((TableStructureEditorViewModel.ColumnModel) -> Void)? @Environment(\.dismiss) internal var dismiss @State internal var draft: Draft @@ -17,12 +18,14 @@ struct ColumnEditorSheet: View { column: Binding, databaseType: DatabaseType, onDelete: @escaping () -> Void, - onCancelNew: @escaping () -> Void + onCancelNew: @escaping () -> Void, + onSaveNew: ((TableStructureEditorViewModel.ColumnModel) -> Void)? = nil ) { self._column = column self.databaseType = databaseType self.onDelete = onDelete self.onCancelNew = onCancelNew + self.onSaveNew = onSaveNew _draft = State(initialValue: Draft(model: column.wrappedValue, databaseType: databaseType)) } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ForeignKeyEditorSheet+Draft.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ForeignKeyEditorSheet+Draft.swift index fc092d5b7..3463d5dfd 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ForeignKeyEditorSheet+Draft.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ForeignKeyEditorSheet+Draft.swift @@ -4,22 +4,30 @@ import Foundation extension ForeignKeyEditorSheet { func applyDraftToModel() { - foreignKey.name = draft.name.trimmingCharacters(in: .whitespacesAndNewlines) - foreignKey.columns = draft.mappings.map(\.localColumn) - foreignKey.referencedSchema = draft.referencedSchema.trimmingCharacters(in: .whitespacesAndNewlines) - foreignKey.referencedTable = draft.referencedTable.trimmingCharacters(in: .whitespacesAndNewlines) - foreignKey.referencedColumns = draft.mappings.map { + let referencedColumns = draft.mappings.map { $0.referencedColumn.trimmingCharacters(in: .whitespacesAndNewlines) } let updateValue = draft.onUpdate.trimmingCharacters(in: .whitespacesAndNewlines) - foreignKey.onUpdate = updateValue.isEmpty || updateValue == ForeignKeyAction.noAction.rawValue ? nil : updateValue - let deleteValue = draft.onDelete.trimmingCharacters(in: .whitespacesAndNewlines) - foreignKey.onDelete = deleteValue.isEmpty || deleteValue == ForeignKeyAction.noAction.rawValue ? nil : deleteValue + let updatedForeignKey = TableStructureEditorViewModel.ForeignKeyModel( + original: foreignKey.original, + name: draft.name.trimmingCharacters(in: .whitespacesAndNewlines), + columns: draft.mappings.map(\.localColumn), + referencedSchema: draft.referencedSchema.trimmingCharacters(in: .whitespacesAndNewlines), + referencedTable: draft.referencedTable.trimmingCharacters(in: .whitespacesAndNewlines), + referencedColumns: referencedColumns, + onUpdate: updateValue.isEmpty || updateValue == ForeignKeyAction.noAction.rawValue ? nil : updateValue, + onDelete: deleteValue.isEmpty || deleteValue == ForeignKeyAction.noAction.rawValue ? nil : deleteValue, + isDeferrable: draft.isDeferrable, + isInitiallyDeferred: draft.isInitiallyDeferred + ) - foreignKey.isDeferrable = draft.isDeferrable - foreignKey.isInitiallyDeferred = draft.isInitiallyDeferred + if draft.isEditingExisting { + foreignKey = updatedForeignKey + } else { + onSaveNew?(updatedForeignKey) + } } struct Draft { diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ForeignKeyEditorSheet.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ForeignKeyEditorSheet.swift index cd5b08f71..b956c836b 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ForeignKeyEditorSheet.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/ForeignKeyEditorSheet.swift @@ -11,6 +11,7 @@ struct ForeignKeyEditorSheet: View { let session: DatabaseSession let onDelete: () -> Void let onCancelNew: () -> Void + let onSaveNew: ((TableStructureEditorViewModel.ForeignKeyModel) -> Void)? @Environment(\.dismiss) var dismiss @State var draft: Draft @@ -24,7 +25,8 @@ struct ForeignKeyEditorSheet: View { databaseType: DatabaseType, session: DatabaseSession, onDelete: @escaping () -> Void, - onCancelNew: @escaping () -> Void + onCancelNew: @escaping () -> Void, + onSaveNew: ((TableStructureEditorViewModel.ForeignKeyModel) -> Void)? = nil ) { self._foreignKey = foreignKey self.availableColumns = availableColumns @@ -32,6 +34,7 @@ struct ForeignKeyEditorSheet: View { self.session = session self.onDelete = onDelete self.onCancelNew = onCancelNew + self.onSaveNew = onSaveNew _draft = State(initialValue: Draft(model: foreignKey.wrappedValue, availableColumns: availableColumns)) } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/IndexEditorSheet+Draft.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/IndexEditorSheet+Draft.swift index 86f69165d..72ffbf3be 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/IndexEditorSheet+Draft.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/IndexEditorSheet+Draft.swift @@ -49,12 +49,21 @@ extension IndexEditorSheet { } func applyDraft() { - index.name = draft.name.trimmingCharacters(in: .whitespacesAndNewlines) - index.isUnique = draft.isUnique - index.filterCondition = draft.filterCondition.trimmingCharacters(in: .whitespacesAndNewlines) - index.indexType = draft.indexType - index.columns = draft.columns.map { column in + let updatedIndex = TableStructureEditorViewModel.IndexModel( + original: index.original, + name: draft.name.trimmingCharacters(in: .whitespacesAndNewlines), + columns: draft.columns.map { column in TableStructureEditorViewModel.IndexModel.Column(name: column.name, sortOrder: column.sortOrder, isIncluded: column.isIncluded) + }, + isUnique: draft.isUnique, + filterCondition: draft.filterCondition.trimmingCharacters(in: .whitespacesAndNewlines), + indexType: draft.indexType + ) + + if draft.isEditingExisting { + index = updatedIndex + } else { + onSaveNew?(updatedIndex) } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/IndexEditorSheet.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/IndexEditorSheet.swift index 5ae17653b..f004b5be2 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/IndexEditorSheet.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/IndexEditorSheet.swift @@ -6,6 +6,7 @@ struct IndexEditorSheet: View { let databaseType: DatabaseType let onDelete: () -> Void let onCancelNew: () -> Void + let onSaveNew: ((TableStructureEditorViewModel.IndexModel) -> Void)? @Environment(\.dismiss) internal var dismiss @State internal var draft: Draft @@ -17,13 +18,15 @@ struct IndexEditorSheet: View { availableColumns: [String], databaseType: DatabaseType, onDelete: @escaping () -> Void, - onCancelNew: @escaping () -> Void + onCancelNew: @escaping () -> Void, + onSaveNew: ((TableStructureEditorViewModel.IndexModel) -> Void)? = nil ) { self._index = index self.availableColumns = availableColumns self.databaseType = databaseType self.onDelete = onDelete self.onCancelNew = onCancelNew + self.onSaveNew = onSaveNew _draft = State(initialValue: Draft(model: index.wrappedValue, availableColumns: availableColumns)) } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/PrimaryKeyEditorSheet+Draft.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/PrimaryKeyEditorSheet+Draft.swift index 2e0a86a89..d03544603 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/PrimaryKeyEditorSheet+Draft.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/PrimaryKeyEditorSheet+Draft.swift @@ -3,10 +3,19 @@ import Foundation extension PrimaryKeyEditorSheet { func applyDraftChanges() { - primaryKey.name = draft.name.trimmingCharacters(in: .whitespacesAndNewlines) - primaryKey.columns = draft.columns.map { $0.name } - primaryKey.isDeferrable = draft.isDeferrable - primaryKey.isInitiallyDeferred = draft.isInitiallyDeferred + let updatedPrimaryKey = TableStructureEditorViewModel.PrimaryKeyModel( + original: primaryKey.original, + name: draft.name.trimmingCharacters(in: .whitespacesAndNewlines), + columns: draft.columns.map { $0.name }, + isDeferrable: draft.isDeferrable, + isInitiallyDeferred: draft.isInitiallyDeferred + ) + + if draft.isEditingExisting { + primaryKey = updatedPrimaryKey + } else { + onSaveNew?(updatedPrimaryKey) + } } func cancelIfNew() { diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/PrimaryKeyEditorSheet.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/PrimaryKeyEditorSheet.swift index 8d65e0363..2c0d09462 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/PrimaryKeyEditorSheet.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/PrimaryKeyEditorSheet.swift @@ -6,6 +6,7 @@ struct PrimaryKeyEditorSheet: View { let databaseType: DatabaseType let onDelete: () -> Void let onCancelNew: () -> Void + let onSaveNew: ((TableStructureEditorViewModel.PrimaryKeyModel) -> Void)? @Environment(\.dismiss) private var dismiss @State var draft: Draft @@ -15,13 +16,15 @@ struct PrimaryKeyEditorSheet: View { availableColumns: [String], databaseType: DatabaseType, onDelete: @escaping () -> Void, - onCancelNew: @escaping () -> Void + onCancelNew: @escaping () -> Void, + onSaveNew: ((TableStructureEditorViewModel.PrimaryKeyModel) -> Void)? = nil ) { self._primaryKey = primaryKey self.availableColumns = availableColumns self.databaseType = databaseType self.onDelete = onDelete self.onCancelNew = onCancelNew + self.onSaveNew = onSaveNew _draft = State(initialValue: Draft(model: primaryKey.wrappedValue, availableColumns: availableColumns)) } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/StructureApplyReviewSheet.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/StructureApplyReviewSheet.swift new file mode 100644 index 000000000..83886fea6 --- /dev/null +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/StructureApplyReviewSheet.swift @@ -0,0 +1,153 @@ +import SwiftUI + +struct StructureApplyReviewSheet: View { + private struct StatementDescriptor: Identifiable { + let id = UUID() + let title: String + let symbol: String + let statement: String + } + + let tableName: String + let statements: [String] + let onApply: () async -> Bool + + @Environment(\.dismiss) private var dismiss + @State private var isApplying = false + @State private var errorMessage: String? + + private var descriptors: [StatementDescriptor] { + statements.map { statement in + StatementDescriptor( + title: statementTitle(for: statement), + symbol: statementSymbol(for: statement), + statement: statement + ) + } + } + + var body: some View { + SheetLayout( + title: "Review Changes", + icon: "list.clipboard", + subtitle: "Echo will apply \(statements.count) change\(statements.count == 1 ? "" : "s") to \(tableName).", + primaryAction: "Apply Changes", + canSubmit: !statements.isEmpty && !isApplying, + isSubmitting: isApplying, + errorMessage: errorMessage, + onSubmit: { await submit() }, + onCancel: { dismiss() } + ) { + ScrollView { + VStack(alignment: .leading, spacing: SpacingTokens.md) { + overviewCard + + ForEach(Array(descriptors.enumerated()), id: \.element.id) { offset, descriptor in + statementCard(descriptor, index: offset + 1) + } + } + .padding(SpacingTokens.lg) + } + .background(ColorTokens.Background.secondary.opacity(0.35)) + } + .frame(minWidth: 700, idealWidth: 820, minHeight: 480, idealHeight: 620) + } + + private var overviewCard: some View { + HStack(spacing: SpacingTokens.sm) { + Image(systemName: "server.rack") + .font(TypographyTokens.prominent) + .foregroundStyle(ColorTokens.Status.info) + + VStack(alignment: .leading, spacing: SpacingTokens.xxxs) { + Text(tableName) + .font(TypographyTokens.prominent.weight(.semibold)) + Text("Review the exact SQL below before Echo sends it to the server.") + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Text.secondary) + } + + Spacer() + + CountBadge(count: statements.count, tint: ColorTokens.Status.info, opacity: 0.12) + } + .padding(SpacingTokens.md) + .background(ColorTokens.Background.primary, in: RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.medium, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.medium, style: .continuous) + .strokeBorder(ColorTokens.Text.primary.opacity(0.08)) + } + } + + private func statementCard(_ descriptor: StatementDescriptor, index: Int) -> some View { + VStack(alignment: .leading, spacing: SpacingTokens.sm) { + HStack(spacing: SpacingTokens.xs) { + Image(systemName: descriptor.symbol) + .font(TypographyTokens.standard) + .foregroundStyle(ColorTokens.Status.info) + + Text("\(index). \(descriptor.title)") + .font(TypographyTokens.standard.weight(.semibold)) + .foregroundStyle(ColorTokens.Text.primary) + + Spacer() + } + + Text(descriptor.statement) + .font(TypographyTokens.code) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(SpacingTokens.md) + .background(ColorTokens.Background.secondary.opacity(0.45), in: RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.small, style: .continuous)) + } + .padding(SpacingTokens.md) + .background(ColorTokens.Background.primary, in: RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.medium, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.medium, style: .continuous) + .strokeBorder(ColorTokens.Text.primary.opacity(0.08)) + } + } + + private func submit() async { + isApplying = true + errorMessage = nil + let didApply = await onApply() + isApplying = false + + if didApply { + dismiss() + } else { + errorMessage = "Failed to apply the reviewed changes." + } + } + + private func statementTitle(for statement: String) -> String { + let uppercased = statement.uppercased() + + if uppercased.contains(" ADD COLUMN ") { return "Add Column" } + if uppercased.contains(" DROP COLUMN ") { return "Drop Column" } + if uppercased.contains(" ALTER COLUMN ") { return "Alter Column" } + if uppercased.contains(" RENAME COLUMN ") || uppercased.contains("SP_RENAME") { return "Rename Column" } + if uppercased.contains("CREATE") && uppercased.contains("INDEX") { return "Create Index" } + if uppercased.contains("DROP INDEX") { return "Drop Index" } + if uppercased.contains("PRIMARY KEY") { return "Primary Key Change" } + if uppercased.contains("FOREIGN KEY") { return "Foreign Key Change" } + if uppercased.contains("CHECK") && uppercased.contains("CONSTRAINT") { return "Check Constraint Change" } + if uppercased.contains("UNIQUE") && uppercased.contains("CONSTRAINT") { return "Unique Constraint Change" } + if uppercased.contains("ADD CONSTRAINT") { return "Add Constraint" } + if uppercased.contains("DROP CONSTRAINT") { return "Drop Constraint" } + return "Schema Change" + } + + private func statementSymbol(for statement: String) -> String { + let uppercased = statement.uppercased() + + if uppercased.contains("INDEX") { return "list.bullet.rectangle" } + if uppercased.contains("FOREIGN KEY") { return "link" } + if uppercased.contains("PRIMARY KEY") { return "key" } + if uppercased.contains("CHECK") || uppercased.contains("UNIQUE") || uppercased.contains("CONSTRAINT") { + return "checkmark.shield" + } + return "tablecells" + } +} diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/StructureScriptPreviewSheet.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/StructureScriptPreviewSheet.swift new file mode 100644 index 000000000..2d4757de4 --- /dev/null +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/StructureScriptPreviewSheet.swift @@ -0,0 +1,53 @@ +import SwiftUI + +struct StructureScriptPreviewSheet: View { + let context: SQLPopoutContext + let onOpenInWindow: (_ sql: String, _ database: String?) -> Void + + @Environment(\.dismiss) private var dismiss + @State private var formattedSQL: String? + + private var displaySQL: String { formattedSQL ?? context.sql } + private var canOpenInQueryWindow: Bool { + !displaySQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } + + var body: some View { + SheetLayoutCustomFooter(title: context.title) { + ScrollView { + Text(displaySQL) + .font(TypographyTokens.code) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(SpacingTokens.lg) + } + .background(ColorTokens.Background.secondary.opacity(0.5)) + } footer: { + Button("Copy SQL") { + PlatformClipboard.copy(displaySQL) + } + .buttonStyle(.bordered) + .disabled(!canOpenInQueryWindow) + + Spacer() + + Button("Cancel") { + dismiss() + } + .keyboardShortcut(.cancelAction) + + Button("Open in Query Window…") { + onOpenInWindow(displaySQL, context.databaseName) + dismiss() + } + .buttonStyle(.bordered) + .disabled(!canOpenInQueryWindow) + } + .frame(minWidth: 640, idealWidth: 760, minHeight: 420, idealHeight: 520) + .task { + if let formatted = try? await SQLFormatter.shared.format(sql: context.sql, dialect: context.formatterDialect) { + formattedSQL = formatted + } + } + } +} diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/UniqueConstraintEditorSheet+Draft.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/UniqueConstraintEditorSheet+Draft.swift index 13851222e..a526c990f 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/UniqueConstraintEditorSheet+Draft.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/UniqueConstraintEditorSheet+Draft.swift @@ -3,10 +3,19 @@ import Foundation extension UniqueConstraintEditorSheet { func applyDraftChanges() { - constraint.name = draft.name.trimmingCharacters(in: .whitespacesAndNewlines) - constraint.columns = draft.columns.map { $0.name } - constraint.isDeferrable = draft.isDeferrable - constraint.isInitiallyDeferred = draft.isInitiallyDeferred + let updatedConstraint = TableStructureEditorViewModel.UniqueConstraintModel( + original: constraint.original, + name: draft.name.trimmingCharacters(in: .whitespacesAndNewlines), + columns: draft.columns.map { $0.name }, + isDeferrable: draft.isDeferrable, + isInitiallyDeferred: draft.isInitiallyDeferred + ) + + if draft.isEditingExisting { + constraint = updatedConstraint + } else { + onSaveNew?(updatedConstraint) + } } func cancelIfNew() { diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/UniqueConstraintEditorSheet.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/UniqueConstraintEditorSheet.swift index 88c0fd257..a0d456952 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/UniqueConstraintEditorSheet.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/Sheets/UniqueConstraintEditorSheet.swift @@ -6,6 +6,7 @@ struct UniqueConstraintEditorSheet: View { let databaseType: DatabaseType let onDelete: () -> Void let onCancelNew: () -> Void + let onSaveNew: ((TableStructureEditorViewModel.UniqueConstraintModel) -> Void)? @Environment(\.dismiss) private var dismiss @State var draft: Draft @@ -15,13 +16,15 @@ struct UniqueConstraintEditorSheet: View { availableColumns: [String], databaseType: DatabaseType, onDelete: @escaping () -> Void, - onCancelNew: @escaping () -> Void + onCancelNew: @escaping () -> Void, + onSaveNew: ((TableStructureEditorViewModel.UniqueConstraintModel) -> Void)? = nil ) { self._constraint = constraint self.availableColumns = availableColumns self.databaseType = databaseType self.onDelete = onDelete self.onCancelNew = onCancelNew + self.onSaveNew = onSaveNew _draft = State(initialValue: Draft(model: constraint.wrappedValue, availableColumns: availableColumns)) } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ColumnActions.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ColumnActions.swift index 657f1dde6..b6bbe4247 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ColumnActions.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ColumnActions.swift @@ -12,7 +12,7 @@ extension TableStructureEditorView { internal func presentBulkEditor(mode: BulkColumnEditorPresentation.Mode, columns: [TableStructureEditorViewModel.ColumnModel]) { guard !columns.isEmpty else { return } - activeSheet = .bulkColumn(BulkColumnEditorPresentation(mode: mode, columnIDs: columns.map(\.id))) + viewModel.sheetCoordinator.activeSheet = .bulkColumn(BulkColumnEditorPresentation(mode: mode, columnIDs: columns.map(\.id))) } internal func pruneSelectedColumns() { @@ -21,12 +21,11 @@ extension TableStructureEditorView { } internal func presentNewColumn() { - let model = viewModel.addColumn() - activeSheet = .column(ColumnEditorPresentation(columnID: model.id, isNew: true)) + viewModel.sheetCoordinator.activeSheet = .newColumn } internal func presentColumnEditor(for column: TableStructureEditorViewModel.ColumnModel) { - activeSheet = .column(ColumnEditorPresentation(columnID: column.id, isNew: column.isNew)) + viewModel.sheetCoordinator.activeSheet = .column(ColumnEditorPresentation(columnID: column.id, isNew: column.isNew)) } internal func columnChangeDescription(for column: TableStructureEditorViewModel.ColumnModel) -> String? { diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Constraints.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Constraints.swift index 4ad179734..4ac8c76f5 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Constraints.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Constraints.swift @@ -6,35 +6,46 @@ extension TableStructureEditorView { internal func presentPrimaryKeyEditor(isNew: Bool) { if isNew { - viewModel.primaryKey = TableStructureEditorViewModel.PrimaryKeyModel( + viewModel.sheetCoordinator.pendingNewPrimaryKey = TableStructureEditorViewModel.PrimaryKeyModel( original: nil, name: "pk_\(viewModel.tableName)", columns: [], isDeferrable: false, isInitiallyDeferred: false ) - viewModel.clearPrimaryKeyRemoval() + viewModel.sheetCoordinator.activeSheet = .newPrimaryKey + return } guard viewModel.primaryKey != nil else { return } - activeSheet = .primaryKey(PrimaryKeyEditorPresentation(isNew: isNew)) + viewModel.sheetCoordinator.activeSheet = .primaryKey(PrimaryKeyEditorPresentation(isNew: isNew)) } internal func presentNewUniqueConstraint() { - let model = viewModel.addUniqueConstraint() - activeSheet = .uniqueConstraint(UniqueConstraintEditorPresentation(constraintID: model.id, isNew: true)) + viewModel.sheetCoordinator.pendingNewUniqueConstraint = TableStructureEditorViewModel.UniqueConstraintModel( + original: nil, + name: "uq_\(viewModel.tableName)_\(viewModel.uniqueConstraints.count + 1)", + columns: [], + isDeferrable: false, + isInitiallyDeferred: false + ) + viewModel.sheetCoordinator.activeSheet = .newUniqueConstraint } internal func presentUniqueConstraintEditor(for constraint: TableStructureEditorViewModel.UniqueConstraintModel) { - activeSheet = .uniqueConstraint(UniqueConstraintEditorPresentation(constraintID: constraint.id, isNew: constraint.isNew)) + viewModel.sheetCoordinator.activeSheet = .uniqueConstraint(UniqueConstraintEditorPresentation(constraintID: constraint.id, isNew: constraint.isNew)) } internal func presentNewCheckConstraint() { - let model = viewModel.addCheckConstraint() - activeSheet = .checkConstraint(CheckConstraintEditorPresentation(constraintID: model.id, isNew: true)) + viewModel.sheetCoordinator.pendingNewCheckConstraint = TableStructureEditorViewModel.CheckConstraintModel( + original: nil, + name: "ck_\(viewModel.tableName)_\(viewModel.checkConstraints.count + 1)", + expression: "" + ) + viewModel.sheetCoordinator.activeSheet = .newCheckConstraint } internal func presentCheckConstraintEditor(for constraint: TableStructureEditorViewModel.CheckConstraintModel) { - activeSheet = .checkConstraint(CheckConstraintEditorPresentation(constraintID: constraint.id, isNew: constraint.isNew)) + viewModel.sheetCoordinator.activeSheet = .checkConstraint(CheckConstraintEditorPresentation(constraintID: constraint.id, isNew: constraint.isNew)) } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ConstraintsTable.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ConstraintsTable.swift index 39a8b826b..0f005495f 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ConstraintsTable.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ConstraintsTable.swift @@ -122,6 +122,14 @@ extension TableStructureEditorView { } .tableStyle(.inset(alternatesRowBackgrounds: true)) .tableColumnAutoResize() + .onContinuousHover { phase in + switch phase { + case .active: + NSCursor.arrow.set() + case .ended: + break + } + } } @ViewBuilder diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ForeignKeys.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ForeignKeys.swift index 57f485326..a44877f5d 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ForeignKeys.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+ForeignKeys.swift @@ -38,7 +38,7 @@ extension TableStructureEditorView { TableColumn("Kind") { _ in Text("FK") .font(TypographyTokens.Table.kindBadge) - .foregroundStyle(.green) + .foregroundStyle(ColorTokens.Status.success) } .width(35) @@ -106,6 +106,14 @@ extension TableStructureEditorView { } .tableStyle(.inset(alternatesRowBackgrounds: true)) .tableColumnAutoResize() + .onContinuousHover { phase in + switch phase { + case .active: + NSCursor.arrow.set() + case .ended: + break + } + } } @ViewBuilder @@ -208,11 +216,24 @@ extension TableStructureEditorView { } internal func presentNewForeignKey() { - let model = viewModel.addForeignKey() - activeSheet = .foreignKey(ForeignKeyEditorPresentation(foreignKeyID: model.id, isNew: true)) + viewModel.sheetCoordinator.pendingNewForeignKey = TableStructureEditorViewModel.ForeignKeyModel( + original: nil, + name: "fk_\(viewModel.tableName)_\(viewModel.foreignKeys.count + 1)", + columns: [], + referencedSchema: viewModel.schemaName, + referencedTable: viewModel.tableName, + referencedColumns: [], + onUpdate: nil, + onDelete: nil, + isDeferrable: false, + isInitiallyDeferred: false + ) + viewModel.sheetCoordinator.activeSheet = .newForeignKey } private func presentForeignKeyEditor(for foreignKey: TableStructureEditorViewModel.ForeignKeyModel) { - activeSheet = .foreignKey(ForeignKeyEditorPresentation(foreignKeyID: foreignKey.id, isNew: foreignKey.isNew)) + viewModel.sheetCoordinator.activeSheet = .foreignKey( + ForeignKeyEditorPresentation(foreignKeyID: foreignKey.id, isNew: foreignKey.isNew) + ) } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Indexes.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Indexes.swift index 2de342a53..01ef0dbb4 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Indexes.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Indexes.swift @@ -10,10 +10,7 @@ extension TableStructureEditorView { } description: { Text("Indexes improve query performance on frequently searched columns.") } actions: { - Button("Add Index") { - let newIndex = viewModel.addIndex() - activeSheet = .index(IndexEditorPresentation(indexID: newIndex.id)) - } + Button("Add Index") { presentNewIndex() } } } else { indexesTable @@ -74,14 +71,11 @@ extension TableStructureEditorView { } .contextMenu(forSelectionType: TableStructureEditorViewModel.IndexModel.ID.self) { selection in if selection.isEmpty { - Button("Add Index") { - let newIndex = viewModel.addIndex() - activeSheet = .index(IndexEditorPresentation(indexID: newIndex.id)) - } + Button("Add Index") { presentNewIndex() } } else if let indexID = selection.first, let index = activeIndexes.first(where: { $0.id == indexID }) { Button("Edit Index") { - activeSheet = .index(IndexEditorPresentation(indexID: index.id)) + viewModel.sheetCoordinator.activeSheet = .index(IndexEditorPresentation(indexID: index.id)) } if !index.isNew { @@ -98,11 +92,19 @@ extension TableStructureEditorView { } } primaryAction: { selection in if let indexID = selection.first { - activeSheet = .index(IndexEditorPresentation(indexID: indexID)) + viewModel.sheetCoordinator.activeSheet = .index(IndexEditorPresentation(indexID: indexID)) } } .tableStyle(.inset(alternatesRowBackgrounds: true)) .tableColumnAutoResize() + .onContinuousHover { phase in + switch phase { + case .active: + NSCursor.arrow.set() + case .ended: + break + } + } } private func rebuildIndex(_ index: TableStructureEditorViewModel.IndexModel) async { @@ -124,4 +126,21 @@ extension TableStructureEditorView { private func indexIncludeColumns(_ index: TableStructureEditorViewModel.IndexModel) -> String { index.columns.filter { $0.isIncluded }.map(\.name).joined(separator: ", ") } + + internal func presentNewIndex() { + let availableColumns = viewModel.columns.filter { !$0.isDeleted } + let initialColumns = availableColumns.prefix(1).map { + TableStructureEditorViewModel.IndexModel.Column(name: $0.name, sortOrder: .ascending, isIncluded: false) + } + let defaultType = viewModel.databaseType == .microsoftSQL ? "nonclustered" : "btree" + viewModel.sheetCoordinator.pendingNewIndex = TableStructureEditorViewModel.IndexModel( + original: nil, + name: "new_index", + columns: Array(initialColumns), + isUnique: false, + filterCondition: "", + indexType: defaultType + ) + viewModel.sheetCoordinator.activeSheet = .newIndex + } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Layout.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Layout.swift index 5bceade61..a404116fe 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Layout.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+Layout.swift @@ -1,7 +1,7 @@ import SwiftUI extension TableStructureEditorView { - + internal func isSectionEnabled(_ section: TableStructureSection) -> Bool { switch section { case .partitions: @@ -12,17 +12,15 @@ extension TableStructureEditorView { return true } } - + internal var header: some View { TabSectionToolbar { structureSectionPicker } controls: { - sectionAddButton - .frame(minWidth: 70, alignment: .trailing) - actionButtons + EmptyView() } } - + private var structureSectionPicker: some View { Picker(selection: $selectedSection) { ForEach(TableStructureSection.sections(for: viewModel.databaseType)) { section in @@ -34,7 +32,7 @@ extension TableStructureEditorView { .pickerStyle(.segmented) .fixedSize() } - + @ViewBuilder private var sectionAddButton: some View { switch selectedSection { @@ -44,17 +42,14 @@ extension TableStructureEditorView { } .controlSize(.small) .buttonStyle(.bordered) - + case .indexes: - Button { - let newIndex = viewModel.addIndex() - activeSheet = .index(IndexEditorPresentation(indexID: newIndex.id)) - } label: { + Button { presentNewIndex() } label: { Label("Add", systemImage: "plus") } .controlSize(.small) .buttonStyle(.bordered) - + case .constraints: Menu { if viewModel.primaryKey == nil { @@ -67,19 +62,19 @@ extension TableStructureEditorView { } .controlSize(.small) .buttonStyle(.bordered) - + case .relations: Button { presentNewForeignKey() } label: { Label("Add", systemImage: "plus") } .controlSize(.small) .buttonStyle(.bordered) - + case .partitions, .inheritance: EmptyView() } } - + internal var content: some View { VStack(spacing: 0) { if viewModel.isLoading && viewModel.columns.isEmpty { @@ -93,16 +88,16 @@ extension TableStructureEditorView { switch selectedSection { case .columns: columnsContent - + case .indexes: indexesContent - + case .constraints: constraintsContent - + case .relations: relationsContent - + case .partitions: if isSectionEnabled(.partitions) { TableStructurePartitionsView(viewModel: viewModel) @@ -113,7 +108,7 @@ extension TableStructureEditorView { Text("This table is not partitioned.") } } - + case .inheritance: if isSectionEnabled(.inheritance) { TableStructureInheritanceView(viewModel: viewModel) @@ -130,32 +125,4 @@ extension TableStructureEditorView { } } } - - @ViewBuilder - private var actionButtons: some View { - Button { - scriptPreviewStatements = viewModel.generateStatements() - activeSheet = .scriptPreview - } label: { - Label("Script", systemImage: "doc.text") - } - .controlSize(.small) - .buttonStyle(.bordered) - .disabled(!viewModel.hasPendingChanges || viewModel.isApplying) - - if viewModel.isApplying { - ProgressView() - .controlSize(.small) - } else { - Button { - applyChanges() - } label: { - Label("Apply", systemImage: "checkmark.circle") - } - .controlSize(.small) - .buttonStyle(.bordered) - .disabled(!viewModel.hasPendingChanges) - .keyboardShortcut(.return, modifiers: [.command, .shift]) - } - } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+SheetModifiers.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+SheetModifiers.swift deleted file mode 100644 index 63855f675..000000000 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView+SheetModifiers.swift +++ /dev/null @@ -1,152 +0,0 @@ -import SwiftUI - -extension TableStructureEditorView { - @ViewBuilder - var sheetModifiers: some View { - Color.clear - .sheet(item: $activeSheet) { sheet in - sheetContent(for: sheet) - } - } - - @ViewBuilder - private func sheetContent(for sheet: TableStructureSheet) -> some View { - switch sheet { - case .index(let presentation): - if let binding = indexBinding(for: presentation.indexID) { - IndexEditorSheet( - index: binding, - availableColumns: viewModel.columns.filter { !$0.isDeleted }.map { $0.name }, - databaseType: tab.connection.databaseType, - onDelete: { - viewModel.removeIndex(binding.wrappedValue) - activeSheet = nil - }, - onCancelNew: { - if binding.wrappedValue.isNew { - viewModel.removeIndex(binding.wrappedValue) - } - activeSheet = nil - } - ) - } - - case .column(let presentation): - if let binding = columnBinding(for: presentation.columnID) { - ColumnEditorSheet( - column: binding, - databaseType: tab.connection.databaseType, - onDelete: { - viewModel.removeColumn(binding.wrappedValue) - activeSheet = nil - }, - onCancelNew: { - if binding.wrappedValue.isNew { - viewModel.removeColumn(binding.wrappedValue) - } - activeSheet = nil - } - ) - } - - case .primaryKey(let presentation): - if let binding = primaryKeyBinding { - PrimaryKeyEditorSheet( - primaryKey: binding, - availableColumns: viewModel.columns.filter { !$0.isDeleted }.map { $0.name }, - databaseType: tab.connection.databaseType, - onDelete: { - viewModel.removePrimaryKey() - activeSheet = nil - }, - onCancelNew: { - if presentation.isNew { - viewModel.removePrimaryKey() - } - activeSheet = nil - } - ) - } - - case .uniqueConstraint(let presentation): - if let binding = uniqueConstraintBinding(for: presentation.constraintID) { - UniqueConstraintEditorSheet( - constraint: binding, - availableColumns: viewModel.columns.filter { !$0.isDeleted }.map { $0.name }, - databaseType: tab.connection.databaseType, - onDelete: { - viewModel.removeUniqueConstraint(binding.wrappedValue) - activeSheet = nil - }, - onCancelNew: { - if binding.wrappedValue.isNew { - viewModel.removeUniqueConstraint(binding.wrappedValue) - } - activeSheet = nil - } - ) - } - - case .foreignKey(let presentation): - if let binding = foreignKeyBinding(for: presentation.foreignKeyID) { - ForeignKeyEditorSheet( - foreignKey: binding, - availableColumns: viewModel.columns.filter { !$0.isDeleted }.map { $0.name }, - databaseType: tab.connection.databaseType, - session: viewModel.session, - onDelete: { - viewModel.removeForeignKey(binding.wrappedValue) - activeSheet = nil - }, - onCancelNew: { - if binding.wrappedValue.isNew { - viewModel.removeForeignKey(binding.wrappedValue) - } - activeSheet = nil - } - ) - } - - case .checkConstraint(let presentation): - if let binding = checkConstraintBinding(for: presentation.constraintID) { - CheckConstraintEditorSheet( - constraint: binding, - onDelete: { - viewModel.removeCheckConstraint(binding.wrappedValue) - activeSheet = nil - }, - onCancelNew: { - if binding.wrappedValue.isNew { - viewModel.removeCheckConstraint(binding.wrappedValue) - } - activeSheet = nil - } - ) - } - - case .scriptPreview: - SQLInspectorSheet( - context: SQLPopoutContext( - sql: scriptPreviewStatements.joined(separator: "\n\n"), - title: "Script Preview" - ) - ) { sql, _ in - if let session = environmentState.sessionGroup.sessionForConnection(tab.connection.id) { - environmentState.openQueryTab(for: session, presetQuery: sql) - } - } - - case .bulkColumn(let presentation): - BulkColumnEditorSheet( - mode: presentation.mode, - columnNames: presentation.columnIDs.compactMap { id in visibleColumns.first(where: { $0.id == id })?.name }, - databaseType: tab.connection.databaseType, - onApply: { value in - let targets = presentation.columnIDs.compactMap { id in columnBinding(for: id) } - applyBulkEdit(mode: presentation.mode, value: value, bindings: targets) - }, - onCancel: { activeSheet = nil } - ) - } - } -} diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView.swift index 4717a75ab..e3d2923a2 100644 --- a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView.swift +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureEditorView.swift @@ -6,43 +6,17 @@ struct TableStructureEditorView: View { @Environment(ProjectStore.self) internal var projectStore @Environment(EnvironmentState.self) internal var environmentState - - @State internal var activeSheet: TableStructureSheet? + @State internal var selectedSection: TableStructureSection @State internal var selectedColumnIDs: Set = [] @State internal var selectedIndexIDs: Set = [] @State internal var selectedForeignKeyIDs: Set = [] @State internal var selectedConstraintIDs: Set = [] - @State internal var scriptPreviewStatements: [String] = [] internal var columnIndexLookup: [UUID: Int] { Dictionary(uniqueKeysWithValues: viewModel.columns.enumerated().map { ($0.element.id, $0.offset) }) } - enum TableStructureSheet: Identifiable { - case index(IndexEditorPresentation) - case column(ColumnEditorPresentation) - case primaryKey(PrimaryKeyEditorPresentation) - case uniqueConstraint(UniqueConstraintEditorPresentation) - case foreignKey(ForeignKeyEditorPresentation) - case checkConstraint(CheckConstraintEditorPresentation) - case scriptPreview - case bulkColumn(BulkColumnEditorPresentation) - - var id: String { - switch self { - case .index(let p): "index-\(p.indexID)" - case .column(let p): "column-\(p.columnID)" - case .primaryKey: "primaryKey" - case .uniqueConstraint(let p): "uniqueConstraint-\(p.constraintID)" - case .foreignKey(let p): "foreignKey-\(p.foreignKeyID)" - case .checkConstraint(let p): "checkConstraint-\(p.constraintID)" - case .scriptPreview: "scriptPreview" - case .bulkColumn: "bulkColumn" - } - } - } - init(tab: WorkspaceTab, viewModel: TableStructureEditorViewModel) { self.tab = tab self.viewModel = viewModel @@ -64,15 +38,31 @@ struct TableStructureEditorView: View { selectedSection = requested viewModel.requestedSection = nil } + consumePendingAddActionIfNeeded() if viewModel.columns.isEmpty && !viewModel.isLoading { Task { await viewModel.reload() } } } + .onChange(of: viewModel.requestedSection) { _, newSection in + guard let newSection else { return } + selectedSection = newSection + viewModel.requestedSection = nil + } + .onChange(of: viewModel.pendingAddAction) { _, _ in + consumePendingAddActionIfNeeded() + } .onChange(of: selectedSection) { viewModel.lastError = nil viewModel.lastSuccessMessage = nil } - .background { sheetModifiers } + .onContinuousHover { phase in + switch phase { + case .active: + NSCursor.arrow.set() + case .ended: + break + } + } } internal func columnBinding(for columnID: UUID) -> Binding? { @@ -154,6 +144,29 @@ struct TableStructureEditorView: View { } } - activeSheet = nil + viewModel.sheetCoordinator.activeSheet = nil + } + + internal func consumePendingAddActionIfNeeded() { + guard let action = viewModel.pendingAddAction else { return } + viewModel.pendingAddAction = nil + + switch action { + case .column: + selectedSection = .columns + presentNewColumn() + case .index: + selectedSection = .indexes + presentNewIndex() + case .foreignKey: + selectedSection = .relations + presentNewForeignKey() + case .uniqueConstraint: + selectedSection = .constraints + presentNewUniqueConstraint() + case .checkConstraint: + selectedSection = .constraints + presentNewCheckConstraint() + } } } diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureSheetCoordinator.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureSheetCoordinator.swift new file mode 100644 index 000000000..7e27514ba --- /dev/null +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureSheetCoordinator.swift @@ -0,0 +1,46 @@ +import SwiftUI + +@MainActor +@Observable +final class TableStructureSheetCoordinator { + var activeSheet: TableStructureSheet? + var pendingNewIndex: TableStructureEditorViewModel.IndexModel? + var pendingNewPrimaryKey: TableStructureEditorViewModel.PrimaryKeyModel? + var pendingNewUniqueConstraint: TableStructureEditorViewModel.UniqueConstraintModel? + var pendingNewForeignKey: TableStructureEditorViewModel.ForeignKeyModel? + var pendingNewCheckConstraint: TableStructureEditorViewModel.CheckConstraintModel? +} + +enum TableStructureSheet: Identifiable { + case index(IndexEditorPresentation) + case column(ColumnEditorPresentation) + case primaryKey(PrimaryKeyEditorPresentation) + case uniqueConstraint(UniqueConstraintEditorPresentation) + case foreignKey(ForeignKeyEditorPresentation) + case checkConstraint(CheckConstraintEditorPresentation) + case newIndex + case newColumn + case newPrimaryKey + case newUniqueConstraint + case newForeignKey + case newCheckConstraint + case bulkColumn(BulkColumnEditorPresentation) + + var id: String { + switch self { + case .index(let presentation): "index-\(presentation.indexID)" + case .column(let presentation): "column-\(presentation.columnID)" + case .primaryKey: "primaryKey" + case .uniqueConstraint(let presentation): "uniqueConstraint-\(presentation.constraintID)" + case .foreignKey(let presentation): "foreignKey-\(presentation.foreignKeyID)" + case .checkConstraint(let presentation): "checkConstraint-\(presentation.constraintID)" + case .newIndex: "newIndex" + case .newColumn: "newColumn" + case .newPrimaryKey: "newPrimaryKey" + case .newUniqueConstraint: "newUniqueConstraint" + case .newForeignKey: "newForeignKey" + case .newCheckConstraint: "newCheckConstraint" + case .bulkColumn: "bulkColumn" + } + } +} diff --git a/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureSheetHost.swift b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureSheetHost.swift new file mode 100644 index 000000000..782c0fbdf --- /dev/null +++ b/Echo/Sources/Features/QueryWorkspace/Views/TableStructure/TableStructureSheetHost.swift @@ -0,0 +1,413 @@ +import SwiftUI + +struct TableStructureSheetHost: View { + @Bindable var tab: WorkspaceTab + @Bindable var viewModel: TableStructureEditorViewModel + + var body: some View { + Color.clear + .sheet( + item: Binding( + get: { viewModel.sheetCoordinator.activeSheet }, + set: { viewModel.sheetCoordinator.activeSheet = $0 } + ) + ) { sheet in + sheetContent(for: sheet) + } + } + + @ViewBuilder + private func sheetContent(for sheet: TableStructureSheet) -> some View { + switch sheet { + case .newIndex: + if let binding = pendingNewIndexBinding { + IndexEditorSheet( + index: binding, + availableColumns: availableColumnNames, + databaseType: tab.connection.databaseType, + onDelete: { dismissNewIndex() }, + onCancelNew: { dismissNewIndex() }, + onSaveNew: { model in + viewModel.indexes.append(model) + dismissNewIndex() + } + ) + } + + case .index(let presentation): + if let binding = indexBinding(for: presentation.indexID) { + IndexEditorSheet( + index: binding, + availableColumns: availableColumnNames, + databaseType: tab.connection.databaseType, + onDelete: { + viewModel.removeIndex(binding.wrappedValue) + dismiss() + }, + onCancelNew: { dismiss() }, + onSaveNew: nil + ) + } + + case .newColumn: + NewColumnSheetHost(databaseType: tab.connection.databaseType) { model in + viewModel.columns.append(model) + dismiss() + } onCancel: { + dismiss() + } + + case .column(let presentation): + if let binding = columnBinding(for: presentation.columnID) { + ColumnEditorSheet( + column: binding, + databaseType: tab.connection.databaseType, + onDelete: { + viewModel.removeColumn(binding.wrappedValue) + dismiss() + }, + onCancelNew: { dismiss() }, + onSaveNew: nil + ) + } + + case .newPrimaryKey: + if let binding = pendingNewPrimaryKeyBinding { + PrimaryKeyEditorSheet( + primaryKey: binding, + availableColumns: availableColumnNames, + databaseType: tab.connection.databaseType, + onDelete: { dismissNewPrimaryKey() }, + onCancelNew: { dismissNewPrimaryKey() }, + onSaveNew: { model in + viewModel.primaryKey = model + viewModel.clearPrimaryKeyRemoval() + dismissNewPrimaryKey() + } + ) + } + + case .primaryKey(let presentation): + if let binding = primaryKeyBinding { + PrimaryKeyEditorSheet( + primaryKey: binding, + availableColumns: availableColumnNames, + databaseType: tab.connection.databaseType, + onDelete: { + viewModel.removePrimaryKey() + dismiss() + }, + onCancelNew: { + if presentation.isNew { + viewModel.removePrimaryKey() + } + dismiss() + }, + onSaveNew: nil + ) + } + + case .newUniqueConstraint: + if let binding = pendingNewUniqueConstraintBinding { + UniqueConstraintEditorSheet( + constraint: binding, + availableColumns: availableColumnNames, + databaseType: tab.connection.databaseType, + onDelete: { dismissNewUniqueConstraint() }, + onCancelNew: { dismissNewUniqueConstraint() }, + onSaveNew: { model in + viewModel.uniqueConstraints.append(model) + dismissNewUniqueConstraint() + } + ) + } + + case .uniqueConstraint(let presentation): + if let binding = uniqueConstraintBinding(for: presentation.constraintID) { + UniqueConstraintEditorSheet( + constraint: binding, + availableColumns: availableColumnNames, + databaseType: tab.connection.databaseType, + onDelete: { + viewModel.removeUniqueConstraint(binding.wrappedValue) + dismiss() + }, + onCancelNew: { + if binding.wrappedValue.isNew { + viewModel.removeUniqueConstraint(binding.wrappedValue) + } + dismiss() + }, + onSaveNew: nil + ) + } + + case .newForeignKey: + if let binding = pendingNewForeignKeyBinding { + ForeignKeyEditorSheet( + foreignKey: binding, + availableColumns: availableColumnNames, + databaseType: tab.connection.databaseType, + session: viewModel.session, + onDelete: { dismissNewForeignKey() }, + onCancelNew: { dismissNewForeignKey() }, + onSaveNew: { model in + viewModel.foreignKeys.append(model) + dismissNewForeignKey() + } + ) + } + + case .foreignKey(let presentation): + if let binding = foreignKeyBinding(for: presentation.foreignKeyID) { + ForeignKeyEditorSheet( + foreignKey: binding, + availableColumns: availableColumnNames, + databaseType: tab.connection.databaseType, + session: viewModel.session, + onDelete: { + viewModel.removeForeignKey(binding.wrappedValue) + dismiss() + }, + onCancelNew: { + if binding.wrappedValue.isNew { + viewModel.removeForeignKey(binding.wrappedValue) + } + dismiss() + }, + onSaveNew: nil + ) + } + + case .newCheckConstraint: + if let binding = pendingNewCheckConstraintBinding { + CheckConstraintEditorSheet( + constraint: binding, + onDelete: { dismissNewCheckConstraint() }, + onCancelNew: { dismissNewCheckConstraint() }, + onSaveNew: { model in + viewModel.checkConstraints.append(model) + dismissNewCheckConstraint() + } + ) + } + + case .checkConstraint(let presentation): + if let binding = checkConstraintBinding(for: presentation.constraintID) { + CheckConstraintEditorSheet( + constraint: binding, + onDelete: { + viewModel.removeCheckConstraint(binding.wrappedValue) + dismiss() + }, + onCancelNew: { + if binding.wrappedValue.isNew { + viewModel.removeCheckConstraint(binding.wrappedValue) + } + dismiss() + }, + onSaveNew: nil + ) + } + + case .bulkColumn(let presentation): + BulkColumnEditorSheet( + mode: presentation.mode, + columnNames: presentation.columnIDs.compactMap { id in + viewModel.columns.first(where: { $0.id == id && !$0.isDeleted })?.name + }, + databaseType: tab.connection.databaseType, + onApply: { value in + let targets = presentation.columnIDs.compactMap { id in columnBinding(for: id) } + applyBulkEdit(mode: presentation.mode, value: value, bindings: targets) + }, + onCancel: { dismiss() } + ) + } + } + + private var availableColumnNames: [String] { + viewModel.columns.filter { !$0.isDeleted }.map(\.name) + } + + private func dismiss() { + viewModel.sheetCoordinator.activeSheet = nil + } + + private func columnBinding(for columnID: UUID) -> Binding? { + guard let index = viewModel.columns.firstIndex(where: { $0.id == columnID }) else { return nil } + return $viewModel.columns[index] + } + + private func indexBinding(for indexID: UUID) -> Binding? { + guard let index = viewModel.indexes.firstIndex(where: { $0.id == indexID }) else { return nil } + return $viewModel.indexes[index] + } + + private func uniqueConstraintBinding(for constraintID: UUID) -> Binding? { + guard let index = viewModel.uniqueConstraints.firstIndex(where: { $0.id == constraintID }) else { return nil } + return $viewModel.uniqueConstraints[index] + } + + private func foreignKeyBinding(for foreignKeyID: UUID) -> Binding? { + guard let index = viewModel.foreignKeys.firstIndex(where: { $0.id == foreignKeyID }) else { return nil } + return $viewModel.foreignKeys[index] + } + + private func checkConstraintBinding(for constraintID: UUID) -> Binding? { + guard let index = viewModel.checkConstraints.firstIndex(where: { $0.id == constraintID }) else { return nil } + return $viewModel.checkConstraints[index] + } + + private var primaryKeyBinding: Binding? { + guard let primaryKey = viewModel.primaryKey else { return nil } + return Binding( + get: { viewModel.primaryKey ?? primaryKey }, + set: { viewModel.primaryKey = $0 } + ) + } + + private var pendingNewIndexBinding: Binding? { + guard let pendingNewIndex = viewModel.sheetCoordinator.pendingNewIndex else { return nil } + return Binding( + get: { viewModel.sheetCoordinator.pendingNewIndex ?? pendingNewIndex }, + set: { viewModel.sheetCoordinator.pendingNewIndex = $0 } + ) + } + + private var pendingNewPrimaryKeyBinding: Binding? { + guard let pendingNewPrimaryKey = viewModel.sheetCoordinator.pendingNewPrimaryKey else { return nil } + return Binding( + get: { viewModel.sheetCoordinator.pendingNewPrimaryKey ?? pendingNewPrimaryKey }, + set: { viewModel.sheetCoordinator.pendingNewPrimaryKey = $0 } + ) + } + + private var pendingNewUniqueConstraintBinding: Binding? { + guard let pendingNewUniqueConstraint = viewModel.sheetCoordinator.pendingNewUniqueConstraint else { return nil } + return Binding( + get: { viewModel.sheetCoordinator.pendingNewUniqueConstraint ?? pendingNewUniqueConstraint }, + set: { viewModel.sheetCoordinator.pendingNewUniqueConstraint = $0 } + ) + } + + private var pendingNewForeignKeyBinding: Binding? { + guard let pendingNewForeignKey = viewModel.sheetCoordinator.pendingNewForeignKey else { return nil } + return Binding( + get: { viewModel.sheetCoordinator.pendingNewForeignKey ?? pendingNewForeignKey }, + set: { viewModel.sheetCoordinator.pendingNewForeignKey = $0 } + ) + } + + private var pendingNewCheckConstraintBinding: Binding? { + guard let pendingNewCheckConstraint = viewModel.sheetCoordinator.pendingNewCheckConstraint else { return nil } + return Binding( + get: { viewModel.sheetCoordinator.pendingNewCheckConstraint ?? pendingNewCheckConstraint }, + set: { viewModel.sheetCoordinator.pendingNewCheckConstraint = $0 } + ) + } + + private func dismissNewIndex() { + viewModel.sheetCoordinator.pendingNewIndex = nil + dismiss() + } + + private func dismissNewPrimaryKey() { + viewModel.sheetCoordinator.pendingNewPrimaryKey = nil + dismiss() + } + + private func dismissNewUniqueConstraint() { + viewModel.sheetCoordinator.pendingNewUniqueConstraint = nil + dismiss() + } + + private func dismissNewForeignKey() { + viewModel.sheetCoordinator.pendingNewForeignKey = nil + dismiss() + } + + private func dismissNewCheckConstraint() { + viewModel.sheetCoordinator.pendingNewCheckConstraint = nil + dismiss() + } + + private func applyBulkEdit( + mode: BulkColumnEditorPresentation.Mode, + value: BulkColumnEditValue, + bindings: [Binding] + ) { + for binding in bindings { + switch mode { + case .dataType: + if case let .dataType(newType) = value { + binding.wrappedValue.dataType = newType + } + case .defaultValue: + if case let .defaultValue(newValue) = value { + binding.wrappedValue.defaultValue = newValue + } + case .generatedExpression: + if case let .generatedExpression(newValue) = value { + binding.wrappedValue.generatedExpression = newValue + } + } + } + + dismiss() + } +} + +private struct NewColumnSheetHost: View { + @State private var column: TableStructureEditorViewModel.ColumnModel + let databaseType: DatabaseType + let onSave: (TableStructureEditorViewModel.ColumnModel) -> Void + let onCancel: () -> Void + + init( + databaseType: DatabaseType, + onSave: @escaping (TableStructureEditorViewModel.ColumnModel) -> Void, + onCancel: @escaping () -> Void + ) { + self.databaseType = databaseType + self.onSave = onSave + self.onCancel = onCancel + + let defaultType: String = switch databaseType { + case .mysql: "varchar(255)" + case .microsoftSQL: "nvarchar(255)" + default: "text" + } + + _column = State( + initialValue: TableStructureEditorViewModel.ColumnModel( + original: nil, + name: "new_column", + dataType: defaultType, + isNullable: true, + defaultValue: nil, + generatedExpression: nil, + isIdentity: false, + identitySeed: nil, + identityIncrement: nil, + identityGeneration: nil, + collation: nil, + characterSet: nil, + comment: nil, + isUnsigned: false, + isZerofill: false, + ordinalPosition: nil + ) + ) + } + + var body: some View { + ColumnEditorSheet( + column: $column, + databaseType: databaseType, + onDelete: onCancel, + onCancelNew: onCancel, + onSaveNew: onSave + ) + } +} diff --git a/Echo/Sources/Features/SchemaDiagram/Views/DiagramAnnotationView.swift b/Echo/Sources/Features/SchemaDiagram/Views/DiagramAnnotationView.swift index 82ff8e480..905eda424 100644 --- a/Echo/Sources/Features/SchemaDiagram/Views/DiagramAnnotationView.swift +++ b/Echo/Sources/Features/SchemaDiagram/Views/DiagramAnnotationView.swift @@ -41,9 +41,9 @@ struct DiagramAnnotationView: View { } .padding(SpacingTokens.xs) .background { - RoundedRectangle(cornerRadius: 6) - .fill(Color.yellow.opacity(0.15)) - .stroke(Color.yellow.opacity(0.4), lineWidth: 1) + RoundedRectangle(cornerRadius: ShapeTokens.CornerRadius.small) + .fill(ColorTokens.Status.warning.opacity(0.15)) + .stroke(ColorTokens.Status.warning.opacity(0.4), lineWidth: 1) } .contextMenu { Button { diff --git a/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+Actions.swift b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+Actions.swift deleted file mode 100644 index b284f4542..000000000 --- a/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+Actions.swift +++ /dev/null @@ -1,861 +0,0 @@ -import Foundation -import PostgresKit - -extension PostgresAdvancedObjectsViewModel { - - // MARK: - Drop Actions - - func dropFDW(_ name: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping FDW \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropForeignDataWrapper(name: name, ifExists: true, cascade: true) - handle?.succeed() - panelState?.appendMessage("Dropped FDW '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop FDW: \(error.localizedDescription)", severity: .error) - } - } - - func dropForeignServer(_ name: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping server \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropForeignServer(name: name, ifExists: true, cascade: true) - handle?.succeed() - panelState?.appendMessage("Dropped foreign server '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop server: \(error.localizedDescription)", severity: .error) - } - } - - func dropEventTrigger(_ name: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping event trigger \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.triggers.dropEventTrigger(name: name, ifExists: true, cascade: true) - handle?.succeed() - panelState?.appendMessage("Dropped event trigger '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop event trigger: \(error.localizedDescription)", severity: .error) - } - } - - func dropDomain(_ name: String, schema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping domain \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.dropDomain(name: name, ifExists: true, cascade: true, schema: schema) - handle?.succeed() - panelState?.appendMessage("Dropped domain '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop domain: \(error.localizedDescription)", severity: .error) - } - } - - func dropCompositeType(_ name: String, schema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping type \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.dropCompositeType(name: name, ifExists: true, cascade: true, schema: schema) - handle?.succeed() - panelState?.appendMessage("Dropped composite type '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop type: \(error.localizedDescription)", severity: .error) - } - } - - func dropRangeType(_ name: String, schema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping range type \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.dropRangeType(name: name, ifExists: true, cascade: true, schema: schema) - handle?.succeed() - panelState?.appendMessage("Dropped range type '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop range type: \(error.localizedDescription)", severity: .error) - } - } - - func dropCollation(_ name: String, schema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping collation \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropCollation(name: name, ifExists: true, cascade: true, schema: schema) - handle?.succeed() - panelState?.appendMessage("Dropped collation '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop collation: \(error.localizedDescription)", severity: .error) - } - } - - func dropFTSConfig(_ name: String, schema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping FTS config \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropTextSearchConfiguration(name: name, ifExists: true, cascade: true, schema: schema) - handle?.succeed() - panelState?.appendMessage("Dropped text search config '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop FTS config: \(error.localizedDescription)", severity: .error) - } - } - - func dropRule(_ name: String, table: String, schema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping rule \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropRule(name: name, table: table, ifExists: true, cascade: true, schema: schema) - handle?.succeed() - panelState?.appendMessage("Dropped rule '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop rule: \(error.localizedDescription)", severity: .error) - } - } - - func dropTablespace(_ name: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping tablespace \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropTablespace(name: name, ifExists: true) - handle?.succeed() - panelState?.appendMessage("Dropped tablespace '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop tablespace: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Create Actions - - func createForeignServer(name: String, type: String?, version: String?, fdwName: String, options: [String: String]?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating foreign server \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createForeignServer(name: name, type: type, version: version, fdwName: fdwName, options: options) - handle?.succeed() - panelState?.appendMessage("Created foreign server '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create server: \(error.localizedDescription)", severity: .error) - } - } - - func createEventTrigger(name: String, event: String, function: String, tags: [String]?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating event trigger \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.triggers.createEventTrigger(name: name, event: event, function: function, tags: tags) - handle?.succeed() - panelState?.appendMessage("Created event trigger '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create event trigger: \(error.localizedDescription)", severity: .error) - } - } - - func createDomain(name: String, schema: String?, dataType: String, defaultValue: String?, notNull: Bool, checkExpression: String?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating domain \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.createDomain(name: name, dataType: dataType, defaultValue: defaultValue, notNull: notNull, checkExpression: checkExpression, schema: schema) - handle?.succeed() - panelState?.appendMessage("Created domain '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create domain: \(error.localizedDescription)", severity: .error) - } - } - - func createCompositeType(name: String, schema: String?, attributes: [(name: String, dataType: String)]) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating composite type \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.createCompositeType(name: name, attributes: attributes, schema: schema) - handle?.succeed() - panelState?.appendMessage("Created composite type '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create type: \(error.localizedDescription)", severity: .error) - } - } - - func createRangeType(name: String, schema: String?, subtype: String, opClass: String?, collation: String?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating range type \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.createRangeType(name: name, subtype: subtype, subtypeOpClass: opClass, collation: collation, schema: schema) - handle?.succeed() - panelState?.appendMessage("Created range type '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create range type: \(error.localizedDescription)", severity: .error) - } - } - - func createCollation(name: String, schema: String?, locale: String?, provider: String?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating collation \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createCollation(name: name, locale: locale, provider: provider, schema: schema) - handle?.succeed() - panelState?.appendMessage("Created collation '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create collation: \(error.localizedDescription)", severity: .error) - } - } - - func createFTSConfig(name: String, schema: String?, parser: String?, copySource: String?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating FTS config \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createTextSearchConfiguration(name: name, parser: parser, copy: copySource, schema: schema) - handle?.succeed() - panelState?.appendMessage("Created text search config '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create FTS config: \(error.localizedDescription)", severity: .error) - } - } - - func createRule(name: String, table: String, schema: String?, event: String, doInstead: Bool, condition: String?, commands: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating rule \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createRule(name: name, table: table, event: event, doInstead: doInstead, condition: condition, commands: commands, schema: schema) - handle?.succeed() - panelState?.appendMessage("Created rule '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create rule: \(error.localizedDescription)", severity: .error) - } - } - - func createTablespace(name: String, location: String, owner: String?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating tablespace \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createTablespace(name: name, location: location, owner: owner) - handle?.succeed() - panelState?.appendMessage("Created tablespace '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create tablespace: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Aggregates - - func dropAggregate(_ id: String) async { - guard let pg = session as? PostgresSession else { return } - guard let agg = aggregates.first(where: { $0.id == id }) else { return } - let handle = activityEngine?.begin("Dropping aggregate \(agg.name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropAggregate(name: agg.name, inputType: agg.inputType, ifExists: true, cascade: true, schema: agg.schema) - handle?.succeed() - panelState?.appendMessage("Dropped aggregate '\(agg.name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop aggregate: \(error.localizedDescription)", severity: .error) - } - } - - func createAggregate(name: String, inputType: String, sfunc: String, stype: String, initcond: String?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating aggregate \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createAggregate(name: name, inputType: inputType, sfunc: sfunc, stype: stype, initcond: initcond, schema: schemaFilter) - handle?.succeed() - panelState?.appendMessage("Created aggregate '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create aggregate: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Operators - - func dropOperator(_ id: String) async { - guard let pg = session as? PostgresSession else { return } - guard let op = operators.first(where: { $0.id == id }) else { return } - let handle = activityEngine?.begin("Dropping operator \(op.name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropOperator(name: op.name, leftType: op.leftType, rightType: op.rightType, ifExists: true, cascade: true, schema: op.schema) - handle?.succeed() - panelState?.appendMessage("Dropped operator '\(op.name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop operator: \(error.localizedDescription)", severity: .error) - } - } - - func createOperator(name: String, leftType: String?, rightType: String?, procedure: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating operator \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createOperator(name: name, leftType: leftType, rightType: rightType, procedure: procedure, schema: schemaFilter) - handle?.succeed() - panelState?.appendMessage("Created operator '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create operator: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Languages - - func dropLanguage(_ name: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Dropping language \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropLanguage(name: name, ifExists: true, cascade: true) - handle?.succeed() - panelState?.appendMessage("Dropped language '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop language: \(error.localizedDescription)", severity: .error) - } - } - - func createLanguage(name: String, trusted: Bool, handler: String?, validator: String?) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating language \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createLanguage(name: name, trusted: trusted, handler: handler, validator: validator) - handle?.succeed() - panelState?.appendMessage("Created language '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create language: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Casts - - func createCast(sourceType: String, targetType: String, function: String?, asAssignment: Bool, asImplicit: Bool) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Creating cast (\(sourceType) AS \(targetType))", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.createCast(sourceType: sourceType, targetType: targetType, function: function, asAssignment: asAssignment, asImplicit: asImplicit) - handle?.succeed() - panelState?.appendMessage("Created cast '\(sourceType) -> \(targetType)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to create cast: \(error.localizedDescription)", severity: .error) - } - } - - func dropCast(_ id: String) async { - guard let pg = session as? PostgresSession else { return } - guard let cast = casts.first(where: { $0.id == id }) else { return } - let handle = activityEngine?.begin("Dropping cast \(cast.sourceType) -> \(cast.targetType)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.dropCast(sourceType: cast.sourceType, targetType: cast.targetType, ifExists: true) - handle?.succeed() - panelState?.appendMessage("Dropped cast '\(cast.sourceType) -> \(cast.targetType)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to drop cast: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Domain Alter Actions - - func renameDomain(_ name: String, schema: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming domain \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.alterDomainRename(name: name, newName: newName, schema: schema) - handle?.succeed() - panelState?.appendMessage("Renamed domain '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename domain: \(error.localizedDescription)", severity: .error) - } - } - - func changeDomainOwner(_ name: String, schema: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of domain \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.alterDomainOwner(name: name, newOwner: newOwner, schema: schema) - handle?.succeed() - panelState?.appendMessage("Changed owner of domain '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change domain owner: \(error.localizedDescription)", severity: .error) - } - } - - func setDomainSchema(_ name: String, schema: String, newSchema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Moving domain \(name) to schema \(newSchema)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.alterDomainSetSchema(name: name, newSchema: newSchema, schema: schema) - handle?.succeed() - panelState?.appendMessage("Moved domain '\(name)' to schema '\(newSchema)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change domain schema: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Composite Type / Range Type Alter Actions - - func renameType(_ name: String, schema: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming type \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.alterTypeRename(name: name, newName: newName, schema: schema) - handle?.succeed() - panelState?.appendMessage("Renamed type '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename type: \(error.localizedDescription)", severity: .error) - } - } - - func changeTypeOwner(_ name: String, schema: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of type \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.alterTypeOwner(name: name, newOwner: newOwner, schema: schema) - handle?.succeed() - panelState?.appendMessage("Changed owner of type '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change type owner: \(error.localizedDescription)", severity: .error) - } - } - - func setTypeSchema(_ name: String, schema: String, newSchema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Moving type \(name) to schema \(newSchema)", connectionSessionID: connectionSessionID) - do { - try await pg.client.types.alterTypeSetSchema(name: name, newSchema: newSchema, schema: schema) - handle?.succeed() - panelState?.appendMessage("Moved type '\(name)' to schema '\(newSchema)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change type schema: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Collation Alter Actions - - func renameCollation(_ name: String, schema: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming collation \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterCollationRename(name: name, newName: newName, schema: schema) - handle?.succeed() - panelState?.appendMessage("Renamed collation '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename collation: \(error.localizedDescription)", severity: .error) - } - } - - func changeCollationOwner(_ name: String, schema: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of collation \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterCollationOwner(name: name, newOwner: newOwner, schema: schema) - handle?.succeed() - panelState?.appendMessage("Changed owner of collation '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change collation owner: \(error.localizedDescription)", severity: .error) - } - } - - func setCollationSchema(_ name: String, schema: String, newSchema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Moving collation \(name) to schema \(newSchema)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterCollationSetSchema(name: name, newSchema: newSchema, schema: schema) - handle?.succeed() - panelState?.appendMessage("Moved collation '\(name)' to schema '\(newSchema)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change collation schema: \(error.localizedDescription)", severity: .error) - } - } - - func refreshCollationVersion(_ name: String, schema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Refreshing collation version \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterCollationRefreshVersion(name: name, schema: schema) - handle?.succeed() - panelState?.appendMessage("Refreshed collation version '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to refresh collation version: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - FTS Config Alter Actions - - func renameFTSConfig(_ name: String, schema: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming FTS config \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterTextSearchConfigurationRename(name: name, newName: newName, schema: schema) - handle?.succeed() - panelState?.appendMessage("Renamed FTS config '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename FTS config: \(error.localizedDescription)", severity: .error) - } - } - - func changeFTSConfigOwner(_ name: String, schema: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of FTS config \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterTextSearchConfigurationOwner(name: name, newOwner: newOwner, schema: schema) - handle?.succeed() - panelState?.appendMessage("Changed owner of FTS config '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change FTS config owner: \(error.localizedDescription)", severity: .error) - } - } - - func setFTSConfigSchema(_ name: String, schema: String, newSchema: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Moving FTS config \(name) to schema \(newSchema)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterTextSearchConfigurationSetSchema(name: name, newSchema: newSchema, schema: schema) - handle?.succeed() - panelState?.appendMessage("Moved FTS config '\(name)' to schema '\(newSchema)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change FTS config schema: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Tablespace Alter Actions - - func renameTablespace(_ name: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming tablespace \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterTablespaceRename(name: name, newName: newName) - handle?.succeed() - panelState?.appendMessage("Renamed tablespace '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename tablespace: \(error.localizedDescription)", severity: .error) - } - } - - func changeTablespaceOwner(_ name: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of tablespace \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterTablespaceOwner(name: name, newOwner: newOwner) - handle?.succeed() - panelState?.appendMessage("Changed owner of tablespace '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change tablespace owner: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Aggregate Alter Actions - - func renameAggregate(_ id: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - guard let agg = aggregates.first(where: { $0.id == id }) else { return } - let handle = activityEngine?.begin("Renaming aggregate \(agg.name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterAggregateRename(name: agg.name, inputType: agg.inputType, newName: newName, schema: agg.schema) - handle?.succeed() - panelState?.appendMessage("Renamed aggregate '\(agg.name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename aggregate: \(error.localizedDescription)", severity: .error) - } - } - - func changeAggregateOwner(_ id: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - guard let agg = aggregates.first(where: { $0.id == id }) else { return } - let handle = activityEngine?.begin("Changing owner of aggregate \(agg.name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterAggregateOwner(name: agg.name, inputType: agg.inputType, newOwner: newOwner, schema: agg.schema) - handle?.succeed() - panelState?.appendMessage("Changed owner of aggregate '\(agg.name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change aggregate owner: \(error.localizedDescription)", severity: .error) - } - } - - func setAggregateSchema(_ id: String, newSchema: String) async { - guard let pg = session as? PostgresSession else { return } - guard let agg = aggregates.first(where: { $0.id == id }) else { return } - let handle = activityEngine?.begin("Moving aggregate \(agg.name) to schema \(newSchema)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterAggregateSetSchema(name: agg.name, inputType: agg.inputType, newSchema: newSchema, schema: agg.schema) - handle?.succeed() - panelState?.appendMessage("Moved aggregate '\(agg.name)' to schema '\(newSchema)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change aggregate schema: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Operator Alter Actions - - func changeOperatorOwner(_ id: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - guard let op = operators.first(where: { $0.id == id }) else { return } - let handle = activityEngine?.begin("Changing owner of operator \(op.name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterOperatorOwner(name: op.name, leftType: op.leftType, rightType: op.rightType, newOwner: newOwner, schema: op.schema) - handle?.succeed() - panelState?.appendMessage("Changed owner of operator '\(op.name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change operator owner: \(error.localizedDescription)", severity: .error) - } - } - - func setOperatorSchema(_ id: String, newSchema: String) async { - guard let pg = session as? PostgresSession else { return } - guard let op = operators.first(where: { $0.id == id }) else { return } - let handle = activityEngine?.begin("Moving operator \(op.name) to schema \(newSchema)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterOperatorSetSchema(name: op.name, leftType: op.leftType, rightType: op.rightType, newSchema: newSchema, schema: op.schema) - handle?.succeed() - panelState?.appendMessage("Moved operator '\(op.name)' to schema '\(newSchema)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change operator schema: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Language Alter Actions - - func renameLanguage(_ name: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming language \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterLanguageRename(name: name, newName: newName) - handle?.succeed() - panelState?.appendMessage("Renamed language '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename language: \(error.localizedDescription)", severity: .error) - } - } - - func changeLanguageOwner(_ name: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of language \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterLanguageOwner(name: name, newOwner: newOwner) - handle?.succeed() - panelState?.appendMessage("Changed owner of language '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change language owner: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Event Trigger Alter Actions - - func renameEventTrigger(_ name: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming event trigger \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.triggers.alterEventTriggerRename(name: name, newName: newName) - handle?.succeed() - panelState?.appendMessage("Renamed event trigger '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename event trigger: \(error.localizedDescription)", severity: .error) - } - } - - func changeEventTriggerOwner(_ name: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of event trigger \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.triggers.alterEventTriggerOwner(name: name, newOwner: newOwner) - handle?.succeed() - panelState?.appendMessage("Changed owner of event trigger '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change event trigger owner: \(error.localizedDescription)", severity: .error) - } - } - - func enableEventTrigger(_ name: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Enabling event trigger \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.triggers.alterEventTriggerEnable(name: name, enable: true) - handle?.succeed() - panelState?.appendMessage("Enabled event trigger '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to enable event trigger: \(error.localizedDescription)", severity: .error) - } - } - - func disableEventTrigger(_ name: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Disabling event trigger \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.triggers.alterEventTriggerEnable(name: name, enable: false) - handle?.succeed() - panelState?.appendMessage("Disabled event trigger '\(name)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to disable event trigger: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - FDW Alter Actions - - func renameFDW(_ name: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming FDW \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterForeignDataWrapperRename(name: name, newName: newName) - handle?.succeed() - panelState?.appendMessage("Renamed FDW '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename FDW: \(error.localizedDescription)", severity: .error) - } - } - - func changeFDWOwner(_ name: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of FDW \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterForeignDataWrapperOwner(name: name, newOwner: newOwner) - handle?.succeed() - panelState?.appendMessage("Changed owner of FDW '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change FDW owner: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Foreign Server Alter Actions - - func renameForeignServer(_ name: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming server \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterForeignServerRename(name: name, newName: newName) - handle?.succeed() - panelState?.appendMessage("Renamed foreign server '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename server: \(error.localizedDescription)", severity: .error) - } - } - - func changeForeignServerOwner(_ name: String, newOwner: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Changing owner of server \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterForeignServerOwner(name: name, newOwner: newOwner) - handle?.succeed() - panelState?.appendMessage("Changed owner of server '\(name)' to '\(newOwner)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to change server owner: \(error.localizedDescription)", severity: .error) - } - } - - // MARK: - Rule Alter Actions - - func renameRule(_ name: String, tableName: String, schema: String, newName: String) async { - guard let pg = session as? PostgresSession else { return } - let handle = activityEngine?.begin("Renaming rule \(name)", connectionSessionID: connectionSessionID) - do { - try await pg.client.admin.alterRuleRename(ruleName: name, tableName: tableName, newName: newName, schema: schema) - handle?.succeed() - panelState?.appendMessage("Renamed rule '\(name)' to '\(newName)'") - await loadCurrentSection() - } catch { - handle?.fail(error.localizedDescription) - panelState?.appendMessage("Failed to rename rule: \(error.localizedDescription)", severity: .error) - } - } -} diff --git a/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+AlterActions.swift b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+AlterActions.swift new file mode 100644 index 000000000..4a739032c --- /dev/null +++ b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+AlterActions.swift @@ -0,0 +1,227 @@ +import Foundation +import PostgresKit + +// MARK: - Alter Actions (Domains, Types, Collations, FTS Configs, Tablespaces) + +extension PostgresAdvancedObjectsViewModel { + + // MARK: - Domain Alter Actions + + func renameDomain(_ name: String, schema: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming domain \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.alterDomainRename(name: name, newName: newName, schema: schema) + handle?.succeed() + panelState?.appendMessage("Renamed domain '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename domain: \(error.localizedDescription)", severity: .error) + } + } + + func changeDomainOwner(_ name: String, schema: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of domain \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.alterDomainOwner(name: name, newOwner: newOwner, schema: schema) + handle?.succeed() + panelState?.appendMessage("Changed owner of domain '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change domain owner: \(error.localizedDescription)", severity: .error) + } + } + + func setDomainSchema(_ name: String, schema: String, newSchema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Moving domain \(name) to schema \(newSchema)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.alterDomainSetSchema(name: name, newSchema: newSchema, schema: schema) + handle?.succeed() + panelState?.appendMessage("Moved domain '\(name)' to schema '\(newSchema)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change domain schema: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - Composite Type / Range Type Alter Actions + + func renameType(_ name: String, schema: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming type \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.alterTypeRename(name: name, newName: newName, schema: schema) + handle?.succeed() + panelState?.appendMessage("Renamed type '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename type: \(error.localizedDescription)", severity: .error) + } + } + + func changeTypeOwner(_ name: String, schema: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of type \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.alterTypeOwner(name: name, newOwner: newOwner, schema: schema) + handle?.succeed() + panelState?.appendMessage("Changed owner of type '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change type owner: \(error.localizedDescription)", severity: .error) + } + } + + func setTypeSchema(_ name: String, schema: String, newSchema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Moving type \(name) to schema \(newSchema)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.alterTypeSetSchema(name: name, newSchema: newSchema, schema: schema) + handle?.succeed() + panelState?.appendMessage("Moved type '\(name)' to schema '\(newSchema)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change type schema: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - Collation Alter Actions + + func renameCollation(_ name: String, schema: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming collation \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterCollationRename(name: name, newName: newName, schema: schema) + handle?.succeed() + panelState?.appendMessage("Renamed collation '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename collation: \(error.localizedDescription)", severity: .error) + } + } + + func changeCollationOwner(_ name: String, schema: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of collation \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterCollationOwner(name: name, newOwner: newOwner, schema: schema) + handle?.succeed() + panelState?.appendMessage("Changed owner of collation '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change collation owner: \(error.localizedDescription)", severity: .error) + } + } + + func setCollationSchema(_ name: String, schema: String, newSchema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Moving collation \(name) to schema \(newSchema)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterCollationSetSchema(name: name, newSchema: newSchema, schema: schema) + handle?.succeed() + panelState?.appendMessage("Moved collation '\(name)' to schema '\(newSchema)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change collation schema: \(error.localizedDescription)", severity: .error) + } + } + + func refreshCollationVersion(_ name: String, schema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Refreshing collation version \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterCollationRefreshVersion(name: name, schema: schema) + handle?.succeed() + panelState?.appendMessage("Refreshed collation version '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to refresh collation version: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - FTS Config Alter Actions + + func renameFTSConfig(_ name: String, schema: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming FTS config \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterTextSearchConfigurationRename(name: name, newName: newName, schema: schema) + handle?.succeed() + panelState?.appendMessage("Renamed FTS config '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename FTS config: \(error.localizedDescription)", severity: .error) + } + } + + func changeFTSConfigOwner(_ name: String, schema: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of FTS config \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterTextSearchConfigurationOwner(name: name, newOwner: newOwner, schema: schema) + handle?.succeed() + panelState?.appendMessage("Changed owner of FTS config '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change FTS config owner: \(error.localizedDescription)", severity: .error) + } + } + + func setFTSConfigSchema(_ name: String, schema: String, newSchema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Moving FTS config \(name) to schema \(newSchema)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterTextSearchConfigurationSetSchema(name: name, newSchema: newSchema, schema: schema) + handle?.succeed() + panelState?.appendMessage("Moved FTS config '\(name)' to schema '\(newSchema)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change FTS config schema: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - Tablespace Alter Actions + + func renameTablespace(_ name: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming tablespace \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterTablespaceRename(name: name, newName: newName) + handle?.succeed() + panelState?.appendMessage("Renamed tablespace '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename tablespace: \(error.localizedDescription)", severity: .error) + } + } + + func changeTablespaceOwner(_ name: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of tablespace \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterTablespaceOwner(name: name, newOwner: newOwner) + handle?.succeed() + panelState?.appendMessage("Changed owner of tablespace '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change tablespace owner: \(error.localizedDescription)", severity: .error) + } + } +} diff --git a/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+AlterObjectActions.swift b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+AlterObjectActions.swift new file mode 100644 index 000000000..c894469de --- /dev/null +++ b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+AlterObjectActions.swift @@ -0,0 +1,250 @@ +import Foundation +import PostgresKit + +// MARK: - Alter Actions (Aggregates, Operators, Languages, Event Triggers, FDWs, Foreign Servers, Rules) + +extension PostgresAdvancedObjectsViewModel { + + // MARK: - Aggregate Alter Actions + + func renameAggregate(_ id: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + guard let agg = aggregates.first(where: { $0.id == id }) else { return } + let handle = activityEngine?.begin("Renaming aggregate \(agg.name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterAggregateRename(name: agg.name, inputType: agg.inputType, newName: newName, schema: agg.schema) + handle?.succeed() + panelState?.appendMessage("Renamed aggregate '\(agg.name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename aggregate: \(error.localizedDescription)", severity: .error) + } + } + + func changeAggregateOwner(_ id: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + guard let agg = aggregates.first(where: { $0.id == id }) else { return } + let handle = activityEngine?.begin("Changing owner of aggregate \(agg.name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterAggregateOwner(name: agg.name, inputType: agg.inputType, newOwner: newOwner, schema: agg.schema) + handle?.succeed() + panelState?.appendMessage("Changed owner of aggregate '\(agg.name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change aggregate owner: \(error.localizedDescription)", severity: .error) + } + } + + func setAggregateSchema(_ id: String, newSchema: String) async { + guard let pg = session as? PostgresSession else { return } + guard let agg = aggregates.first(where: { $0.id == id }) else { return } + let handle = activityEngine?.begin("Moving aggregate \(agg.name) to schema \(newSchema)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterAggregateSetSchema(name: agg.name, inputType: agg.inputType, newSchema: newSchema, schema: agg.schema) + handle?.succeed() + panelState?.appendMessage("Moved aggregate '\(agg.name)' to schema '\(newSchema)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change aggregate schema: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - Operator Alter Actions + + func changeOperatorOwner(_ id: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + guard let op = operators.first(where: { $0.id == id }) else { return } + let handle = activityEngine?.begin("Changing owner of operator \(op.name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterOperatorOwner(name: op.name, leftType: op.leftType, rightType: op.rightType, newOwner: newOwner, schema: op.schema) + handle?.succeed() + panelState?.appendMessage("Changed owner of operator '\(op.name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change operator owner: \(error.localizedDescription)", severity: .error) + } + } + + func setOperatorSchema(_ id: String, newSchema: String) async { + guard let pg = session as? PostgresSession else { return } + guard let op = operators.first(where: { $0.id == id }) else { return } + let handle = activityEngine?.begin("Moving operator \(op.name) to schema \(newSchema)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterOperatorSetSchema(name: op.name, leftType: op.leftType, rightType: op.rightType, newSchema: newSchema, schema: op.schema) + handle?.succeed() + panelState?.appendMessage("Moved operator '\(op.name)' to schema '\(newSchema)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change operator schema: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - Language Alter Actions + + func renameLanguage(_ name: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming language \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterLanguageRename(name: name, newName: newName) + handle?.succeed() + panelState?.appendMessage("Renamed language '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename language: \(error.localizedDescription)", severity: .error) + } + } + + func changeLanguageOwner(_ name: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of language \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterLanguageOwner(name: name, newOwner: newOwner) + handle?.succeed() + panelState?.appendMessage("Changed owner of language '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change language owner: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - Event Trigger Alter Actions + + func renameEventTrigger(_ name: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming event trigger \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.triggers.alterEventTriggerRename(name: name, newName: newName) + handle?.succeed() + panelState?.appendMessage("Renamed event trigger '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename event trigger: \(error.localizedDescription)", severity: .error) + } + } + + func changeEventTriggerOwner(_ name: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of event trigger \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.triggers.alterEventTriggerOwner(name: name, newOwner: newOwner) + handle?.succeed() + panelState?.appendMessage("Changed owner of event trigger '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change event trigger owner: \(error.localizedDescription)", severity: .error) + } + } + + func enableEventTrigger(_ name: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Enabling event trigger \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.triggers.alterEventTriggerEnable(name: name, enable: true) + handle?.succeed() + panelState?.appendMessage("Enabled event trigger '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to enable event trigger: \(error.localizedDescription)", severity: .error) + } + } + + func disableEventTrigger(_ name: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Disabling event trigger \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.triggers.alterEventTriggerEnable(name: name, enable: false) + handle?.succeed() + panelState?.appendMessage("Disabled event trigger '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to disable event trigger: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - FDW Alter Actions + + func renameFDW(_ name: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming FDW \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterForeignDataWrapperRename(name: name, newName: newName) + handle?.succeed() + panelState?.appendMessage("Renamed FDW '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename FDW: \(error.localizedDescription)", severity: .error) + } + } + + func changeFDWOwner(_ name: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of FDW \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterForeignDataWrapperOwner(name: name, newOwner: newOwner) + handle?.succeed() + panelState?.appendMessage("Changed owner of FDW '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change FDW owner: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - Foreign Server Alter Actions + + func renameForeignServer(_ name: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming server \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterForeignServerRename(name: name, newName: newName) + handle?.succeed() + panelState?.appendMessage("Renamed foreign server '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename server: \(error.localizedDescription)", severity: .error) + } + } + + func changeForeignServerOwner(_ name: String, newOwner: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Changing owner of server \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterForeignServerOwner(name: name, newOwner: newOwner) + handle?.succeed() + panelState?.appendMessage("Changed owner of server '\(name)' to '\(newOwner)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to change server owner: \(error.localizedDescription)", severity: .error) + } + } + + // MARK: - Rule Alter Actions + + func renameRule(_ name: String, tableName: String, schema: String, newName: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Renaming rule \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.alterRuleRename(ruleName: name, tableName: tableName, newName: newName, schema: schema) + handle?.succeed() + panelState?.appendMessage("Renamed rule '\(name)' to '\(newName)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to rename rule: \(error.localizedDescription)", severity: .error) + } + } +} diff --git a/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+CreateActions.swift b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+CreateActions.swift new file mode 100644 index 000000000..7330a1823 --- /dev/null +++ b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+CreateActions.swift @@ -0,0 +1,189 @@ +import Foundation +import PostgresKit + +// MARK: - Create Actions + +extension PostgresAdvancedObjectsViewModel { + + func createForeignServer(name: String, type: String?, version: String?, fdwName: String, options: [String: String]?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating foreign server \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createForeignServer(name: name, type: type, version: version, fdwName: fdwName, options: options) + handle?.succeed() + panelState?.appendMessage("Created foreign server '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create server: \(error.localizedDescription)", severity: .error) + } + } + + func createEventTrigger(name: String, event: String, function: String, tags: [String]?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating event trigger \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.triggers.createEventTrigger(name: name, event: event, function: function, tags: tags) + handle?.succeed() + panelState?.appendMessage("Created event trigger '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create event trigger: \(error.localizedDescription)", severity: .error) + } + } + + func createDomain(name: String, schema: String?, dataType: String, defaultValue: String?, notNull: Bool, checkExpression: String?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating domain \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.createDomain(name: name, dataType: dataType, defaultValue: defaultValue, notNull: notNull, checkExpression: checkExpression, schema: schema) + handle?.succeed() + panelState?.appendMessage("Created domain '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create domain: \(error.localizedDescription)", severity: .error) + } + } + + func createCompositeType(name: String, schema: String?, attributes: [(name: String, dataType: String)]) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating composite type \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.createCompositeType(name: name, attributes: attributes, schema: schema) + handle?.succeed() + panelState?.appendMessage("Created composite type '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create type: \(error.localizedDescription)", severity: .error) + } + } + + func createRangeType(name: String, schema: String?, subtype: String, opClass: String?, collation: String?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating range type \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.createRangeType(name: name, subtype: subtype, subtypeOpClass: opClass, collation: collation, schema: schema) + handle?.succeed() + panelState?.appendMessage("Created range type '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create range type: \(error.localizedDescription)", severity: .error) + } + } + + func createCollation(name: String, schema: String?, locale: String?, provider: String?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating collation \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createCollation(name: name, locale: locale, provider: provider, schema: schema) + handle?.succeed() + panelState?.appendMessage("Created collation '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create collation: \(error.localizedDescription)", severity: .error) + } + } + + func createFTSConfig(name: String, schema: String?, parser: String?, copySource: String?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating FTS config \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createTextSearchConfiguration(name: name, parser: parser, copy: copySource, schema: schema) + handle?.succeed() + panelState?.appendMessage("Created text search config '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create FTS config: \(error.localizedDescription)", severity: .error) + } + } + + func createRule(name: String, table: String, schema: String?, event: String, doInstead: Bool, condition: String?, commands: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating rule \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createRule(name: name, table: table, event: event, doInstead: doInstead, condition: condition, commands: commands, schema: schema) + handle?.succeed() + panelState?.appendMessage("Created rule '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create rule: \(error.localizedDescription)", severity: .error) + } + } + + func createTablespace(name: String, location: String, owner: String?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating tablespace \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createTablespace(name: name, location: location, owner: owner) + handle?.succeed() + panelState?.appendMessage("Created tablespace '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create tablespace: \(error.localizedDescription)", severity: .error) + } + } + + func createAggregate(name: String, inputType: String, sfunc: String, stype: String, initcond: String?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating aggregate \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createAggregate(name: name, inputType: inputType, sfunc: sfunc, stype: stype, initcond: initcond, schema: schemaFilter) + handle?.succeed() + panelState?.appendMessage("Created aggregate '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create aggregate: \(error.localizedDescription)", severity: .error) + } + } + + func createOperator(name: String, leftType: String?, rightType: String?, procedure: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating operator \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createOperator(name: name, leftType: leftType, rightType: rightType, procedure: procedure, schema: schemaFilter) + handle?.succeed() + panelState?.appendMessage("Created operator '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create operator: \(error.localizedDescription)", severity: .error) + } + } + + func createLanguage(name: String, trusted: Bool, handler: String?, validator: String?) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating language \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createLanguage(name: name, trusted: trusted, handler: handler, validator: validator) + handle?.succeed() + panelState?.appendMessage("Created language '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create language: \(error.localizedDescription)", severity: .error) + } + } + + func createCast(sourceType: String, targetType: String, function: String?, asAssignment: Bool, asImplicit: Bool) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Creating cast (\(sourceType) AS \(targetType))", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.createCast(sourceType: sourceType, targetType: targetType, function: function, asAssignment: asAssignment, asImplicit: asImplicit) + handle?.succeed() + panelState?.appendMessage("Created cast '\(sourceType) -> \(targetType)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to create cast: \(error.localizedDescription)", severity: .error) + } + } +} diff --git a/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+DropActions.swift b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+DropActions.swift new file mode 100644 index 000000000..c24816cba --- /dev/null +++ b/Echo/Sources/Features/Security/Domain/PostgresAdvancedObjectsViewModel+DropActions.swift @@ -0,0 +1,206 @@ +import Foundation +import PostgresKit + +// MARK: - Drop Actions + +extension PostgresAdvancedObjectsViewModel { + + func dropFDW(_ name: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping FDW \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropForeignDataWrapper(name: name, ifExists: true, cascade: true) + handle?.succeed() + panelState?.appendMessage("Dropped FDW '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop FDW: \(error.localizedDescription)", severity: .error) + } + } + + func dropForeignServer(_ name: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping server \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropForeignServer(name: name, ifExists: true, cascade: true) + handle?.succeed() + panelState?.appendMessage("Dropped foreign server '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop server: \(error.localizedDescription)", severity: .error) + } + } + + func dropEventTrigger(_ name: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping event trigger \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.triggers.dropEventTrigger(name: name, ifExists: true, cascade: true) + handle?.succeed() + panelState?.appendMessage("Dropped event trigger '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop event trigger: \(error.localizedDescription)", severity: .error) + } + } + + func dropDomain(_ name: String, schema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping domain \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.dropDomain(name: name, ifExists: true, cascade: true, schema: schema) + handle?.succeed() + panelState?.appendMessage("Dropped domain '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop domain: \(error.localizedDescription)", severity: .error) + } + } + + func dropCompositeType(_ name: String, schema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping type \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.dropCompositeType(name: name, ifExists: true, cascade: true, schema: schema) + handle?.succeed() + panelState?.appendMessage("Dropped composite type '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop type: \(error.localizedDescription)", severity: .error) + } + } + + func dropRangeType(_ name: String, schema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping range type \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.types.dropRangeType(name: name, ifExists: true, cascade: true, schema: schema) + handle?.succeed() + panelState?.appendMessage("Dropped range type '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop range type: \(error.localizedDescription)", severity: .error) + } + } + + func dropCollation(_ name: String, schema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping collation \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropCollation(name: name, ifExists: true, cascade: true, schema: schema) + handle?.succeed() + panelState?.appendMessage("Dropped collation '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop collation: \(error.localizedDescription)", severity: .error) + } + } + + func dropFTSConfig(_ name: String, schema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping FTS config \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropTextSearchConfiguration(name: name, ifExists: true, cascade: true, schema: schema) + handle?.succeed() + panelState?.appendMessage("Dropped text search config '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop FTS config: \(error.localizedDescription)", severity: .error) + } + } + + func dropRule(_ name: String, table: String, schema: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping rule \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropRule(name: name, table: table, ifExists: true, cascade: true, schema: schema) + handle?.succeed() + panelState?.appendMessage("Dropped rule '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop rule: \(error.localizedDescription)", severity: .error) + } + } + + func dropTablespace(_ name: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping tablespace \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropTablespace(name: name, ifExists: true) + handle?.succeed() + panelState?.appendMessage("Dropped tablespace '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop tablespace: \(error.localizedDescription)", severity: .error) + } + } + + func dropAggregate(_ id: String) async { + guard let pg = session as? PostgresSession else { return } + guard let agg = aggregates.first(where: { $0.id == id }) else { return } + let handle = activityEngine?.begin("Dropping aggregate \(agg.name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropAggregate(name: agg.name, inputType: agg.inputType, ifExists: true, cascade: true, schema: agg.schema) + handle?.succeed() + panelState?.appendMessage("Dropped aggregate '\(agg.name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop aggregate: \(error.localizedDescription)", severity: .error) + } + } + + func dropOperator(_ id: String) async { + guard let pg = session as? PostgresSession else { return } + guard let op = operators.first(where: { $0.id == id }) else { return } + let handle = activityEngine?.begin("Dropping operator \(op.name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropOperator(name: op.name, leftType: op.leftType, rightType: op.rightType, ifExists: true, cascade: true, schema: op.schema) + handle?.succeed() + panelState?.appendMessage("Dropped operator '\(op.name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop operator: \(error.localizedDescription)", severity: .error) + } + } + + func dropLanguage(_ name: String) async { + guard let pg = session as? PostgresSession else { return } + let handle = activityEngine?.begin("Dropping language \(name)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropLanguage(name: name, ifExists: true, cascade: true) + handle?.succeed() + panelState?.appendMessage("Dropped language '\(name)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop language: \(error.localizedDescription)", severity: .error) + } + } + + func dropCast(_ id: String) async { + guard let pg = session as? PostgresSession else { return } + guard let cast = casts.first(where: { $0.id == id }) else { return } + let handle = activityEngine?.begin("Dropping cast \(cast.sourceType) -> \(cast.targetType)", connectionSessionID: connectionSessionID) + do { + try await pg.client.admin.dropCast(sourceType: cast.sourceType, targetType: cast.targetType, ifExists: true) + handle?.succeed() + panelState?.appendMessage("Dropped cast '\(cast.sourceType) -> \(cast.targetType)'") + await loadCurrentSection() + } catch { + handle?.fail(error.localizedDescription) + panelState?.appendMessage("Failed to drop cast: \(error.localizedDescription)", severity: .error) + } + } +} diff --git a/Echo/Sources/Shared/DesignSystem/Components/DatabaseTypeIcon.swift b/Echo/Sources/Shared/DesignSystem/Components/DatabaseTypeIcon.swift new file mode 100644 index 000000000..f11e81bb7 --- /dev/null +++ b/Echo/Sources/Shared/DesignSystem/Components/DatabaseTypeIcon.swift @@ -0,0 +1,58 @@ +import SwiftUI + +struct DatabaseTypeIcon: View { + let databaseType: DatabaseType + var tint: Color? = nil + var isColorful: Bool = true + var presentation: RasterSymbolPresentation = .standard + var glyphScale: CGFloat = 1 + + @ViewBuilder + var body: some View { +#if os(macOS) + if let nativeImage = nativeImage { + Image(nsImage: nativeImage) + .renderingMode(databaseType.usesTemplateIcon ? .template : .original) + .foregroundStyle(foregroundTint) + .grayscale(isColorful || databaseType.usesTemplateIcon ? 0 : 1) + .accessibilityHidden(true) + } else { + fallbackBody + } +#else + fallbackBody +#endif + } + + @ViewBuilder + private var fallbackBody: some View { + SymbolLikeAssetImage( + assetName: databaseType.iconName, + isTemplate: databaseType.usesTemplateIcon, + tint: tint, + isColorful: isColorful, + presentation: presentation, + glyphScale: glyphScale + ) + } + + private var foregroundTint: Color { + if databaseType.usesTemplateIcon { + return isColorful ? (tint ?? Color.primary) : ColorTokens.Sidebar.symbol + } + return .primary + } + +#if os(macOS) + private var nativeImage: NSImage? { + switch presentation { + case .menu: + databaseType.menuIconImage() + case .formControl: + databaseType.formControlIconImage() + case .standard, .landingRecent, .sidebar: + nil + } + } +#endif +} diff --git a/Echo/Sources/Shared/DesignSystem/Components/MSSQLDataTypePicker.swift b/Echo/Sources/Shared/DesignSystem/Components/MSSQLDataTypePicker.swift index 0b3980cd9..4367fce64 100644 --- a/Echo/Sources/Shared/DesignSystem/Components/MSSQLDataTypePicker.swift +++ b/Echo/Sources/Shared/DesignSystem/Components/MSSQLDataTypePicker.swift @@ -126,6 +126,11 @@ struct MSSQLDataTypePicker: View { baseType = "" selection = "" } else { + let currentSelection = Self.selectionState(for: selection) + if currentSelection.baseType.caseInsensitiveCompare(newValue) == .orderedSame { + sizeParam = currentSelection.sizeParam + return + } sizeParam = Self.parameterInfo[newValue.lowercased()]?.defaultValue ?? "" syncToSelection() } @@ -137,17 +142,10 @@ struct MSSQLDataTypePicker: View { private func syncFromSelection() { isSyncing = true defer { isSyncing = false } - let parsed = parseType(selection) - if Self.allFlat.contains(where: { $0.lowercased() == parsed.base.lowercased() }) { - baseType = Self.allFlat.first { $0.lowercased() == parsed.base.lowercased() } ?? parsed.base - sizeParam = parsed.param.isEmpty ? (Self.parameterInfo[baseType.lowercased()]?.defaultValue ?? "") : parsed.param - } else if selection.isEmpty { - baseType = "nvarchar" - sizeParam = "255" - syncToSelection() - } else { - isCustom = true - } + let resolvedState = Self.selectionState(for: selection) + baseType = resolvedState.baseType + sizeParam = resolvedState.sizeParam + isCustom = resolvedState.isCustom } private func syncToSelection() { @@ -162,7 +160,23 @@ struct MSSQLDataTypePicker: View { selection = newValue } - private func parseType(_ type: String) -> (base: String, param: String) { + internal static func selectionState(for selection: String) -> (baseType: String, sizeParam: String, isCustom: Bool) { + let trimmedSelection = selection.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedSelection.isEmpty else { + return ("", "", false) + } + + let parsed = parseType(trimmedSelection) + guard let matchedType = allFlat.first(where: { $0.lowercased() == parsed.base.lowercased() }) else { + return ("", "", true) + } + + // Preserve metadata-provided bare types such as `nvarchar` so opening the + // editor doesn't rewrite them to an arbitrary default like `nvarchar(255)`. + return (matchedType, parsed.param, false) + } + + private static func parseType(_ type: String) -> (base: String, param: String) { guard let openParen = type.firstIndex(of: "("), let closeParen = type.lastIndex(of: ")") else { return (type, "") diff --git a/Echo/Sources/Shared/DesignSystem/Components/SidebarConnectionHeader.swift b/Echo/Sources/Shared/DesignSystem/Components/SidebarConnectionHeader.swift index 055a95c01..1653d4118 100644 --- a/Echo/Sources/Shared/DesignSystem/Components/SidebarConnectionHeader.swift +++ b/Echo/Sources/Shared/DesignSystem/Components/SidebarConnectionHeader.swift @@ -16,6 +16,12 @@ struct SidebarConnectionHeader: View { let connectionState: ConnectionState let onAction: () -> Void var trailingAccessory: TrailingAccessory = .chevron + var iconScale: CGFloat = 1 + var iconFrameScale: CGFloat = 1 + var iconGlyphScale: CGFloat = 1 + var leadingPaddingAdjustment: CGFloat = 0 + var statusPresentation: StatusPresentation = .overlayIcon + var labelFont: Font? = nil @Environment(\.sidebarDensity) private var density @@ -26,6 +32,12 @@ struct SidebarConnectionHeader: View { case none } + enum StatusPresentation { + case overlayIcon + case inlineDot + case none + } + private var statusInfo: (color: Color, label: String?) { switch connectionState { case .connected: @@ -74,6 +86,9 @@ struct SidebarConnectionHeader: View { } private var densityLabelFont: Font { + if let labelFont { + return labelFont + } switch density { case .small: return SidebarRowConstants.labelFont // 11pt case .medium: return Font.system(size: 12, weight: .regular) @@ -91,10 +106,6 @@ struct SidebarConnectionHeader: View { // MARK: - Icon - private var iconColor: Color { - isColorful ? connectionColor : ColorTokens.Sidebar.symbol - } - // MARK: - Highlight private var highlightFill: some View { @@ -114,21 +125,7 @@ struct SidebarConnectionHeader: View { } .frame(width: SidebarRowConstants.chevronWidth) - // Server icon with status dot - ZStack(alignment: .bottomTrailing) { - Image(systemName: databaseType.symbolName) - .font(densityIconFont) - .imageScale(.medium) - .symbolRenderingMode(.monochrome) - .foregroundStyle(iconColor) - .frame(width: densityIconFrameWidth, height: densityIconFrameHeight) - - Circle() - .fill(statusInfo.color) - .frame(width: densityStatusDotSize, height: densityStatusDotSize) - .overlay(Circle().stroke(Color.white.opacity(0.4), lineWidth: 0.75)) - .offset(x: 1.5, y: 1.5) - } + serverIconView // Connection name — single line, same font as SidebarRow Text(connectionName) @@ -136,6 +133,10 @@ struct SidebarConnectionHeader: View { .foregroundStyle(ColorTokens.Text.primary) .lineLimit(1) + if statusPresentation == .inlineDot { + inlineStatusIndicator + } + if isSecure { Image(systemName: "lock.fill") .font(.system(size: density == .large ? 9 : 8)) @@ -152,7 +153,7 @@ struct SidebarConnectionHeader: View { trailingAccessoryView } - .padding(.leading, SidebarRowConstants.rowLeadingPadding) + .padding(.leading, SidebarRowConstants.rowLeadingPadding + leadingPaddingAdjustment) .padding(.trailing, SidebarRowConstants.rowTrailingPadding) .padding(.vertical, densityVerticalPadding) .background(highlightFill) @@ -165,6 +166,58 @@ struct SidebarConnectionHeader: View { .focusable(false) } + @ViewBuilder + private var serverIconView: some View { + switch statusPresentation { + case .overlayIcon: + ZStack(alignment: .bottomTrailing) { + iconImage + statusDot + .offset(x: 1.5, y: 1.5) + } + case .inlineDot, .none: + iconImage + } + } + + private var iconImage: some View { + DatabaseTypeIcon( + databaseType: databaseType, + tint: connectionColor, + isColorful: isColorful, + presentation: .sidebar, + glyphScale: iconGlyphScale + ) + .scaleEffect(iconScale) + .frame( + width: densityIconFrameWidth * iconFrameScale, + height: densityIconFrameHeight * iconFrameScale + ) + } + + private var statusDot: some View { + Circle() + .fill(statusInfo.color) + .frame(width: densityStatusDotSize, height: densityStatusDotSize) + .overlay(Circle().stroke(Color.white.opacity(0.4), lineWidth: 0.75)) + } + + @ViewBuilder + private var inlineStatusIndicator: some View { + switch connectionState { + case .connected, .disconnected, .error: + statusDot + .shadow(color: statusInfo.color.opacity(0.18), radius: 1.5, y: 0.5) + .padding(.leading, SpacingTokens.xxxs) + .padding(.trailing, SpacingTokens.xxxs) + case .connecting, .testing: + ProgressView() + .controlSize(.mini) + .padding(.leading, SpacingTokens.xxxs) + .padding(.trailing, SpacingTokens.xxxs) + } + } + // MARK: - Trailing Accessory @ViewBuilder diff --git a/Echo/Sources/Shared/DesignSystem/Components/SymbolLikeAssetImage.swift b/Echo/Sources/Shared/DesignSystem/Components/SymbolLikeAssetImage.swift new file mode 100644 index 000000000..0fb5a3685 --- /dev/null +++ b/Echo/Sources/Shared/DesignSystem/Components/SymbolLikeAssetImage.swift @@ -0,0 +1,82 @@ +import SwiftUI + +enum RasterSymbolPresentation: Sendable { + case standard + case menu + case formControl + case landingRecent + case sidebar + + var canvasWidth: CGFloat { + switch self { + case .standard, .menu, .formControl: + return LayoutTokens.Icon.standardCanvas + case .landingRecent: + return LayoutTokens.Icon.landingRecentCanvas + case .sidebar: + return LayoutTokens.Icon.sidebarCanvasWidth + } + } + + var canvasHeight: CGFloat { + switch self { + case .standard: + return LayoutTokens.Icon.standardCanvas + case .menu: + return LayoutTokens.Icon.menuCanvas + case .formControl: + return LayoutTokens.Icon.formControlCanvas + case .landingRecent: + return LayoutTokens.Icon.landingRecentCanvas + case .sidebar: + return LayoutTokens.Icon.sidebarCanvasHeight + } + } + + var glyphSize: CGFloat { + switch self { + case .standard: + return LayoutTokens.Icon.standardGlyph + case .menu: + return LayoutTokens.Icon.menuGlyph + case .formControl: + return LayoutTokens.Icon.formControlGlyph + case .landingRecent: + return LayoutTokens.Icon.landingRecentGlyph + case .sidebar: + return LayoutTokens.Icon.sidebarGlyph + } + } +} + +struct SymbolLikeAssetImage: View { + let assetName: String + let isTemplate: Bool + var tint: Color? = nil + var isColorful: Bool = true + var presentation: RasterSymbolPresentation = .standard + var glyphScale: CGFloat = 1 + + @ViewBuilder + var body: some View { + ZStack { + if isTemplate { + Image(assetName) + .renderingMode(.template) + .resizable() + .scaledToFit() + .foregroundStyle(isColorful ? (tint ?? Color.primary) : ColorTokens.Sidebar.symbol) + .frame(width: presentation.glyphSize * glyphScale, height: presentation.glyphSize * glyphScale) + } else { + Image(assetName) + .renderingMode(.original) + .resizable() + .scaledToFit() + .grayscale(isColorful ? 0 : 1) + .frame(width: presentation.glyphSize * glyphScale, height: presentation.glyphSize * glyphScale) + } + } + .frame(width: presentation.canvasWidth, height: presentation.canvasHeight) + .accessibilityHidden(true) + } +} diff --git a/Echo/Sources/Shared/DesignSystem/LayoutToken.swift b/Echo/Sources/Shared/DesignSystem/LayoutToken.swift index 08fe3a76a..86eacf93b 100644 --- a/Echo/Sources/Shared/DesignSystem/LayoutToken.swift +++ b/Echo/Sources/Shared/DesignSystem/LayoutToken.swift @@ -1,6 +1,35 @@ import SwiftUI public enum LayoutTokens { + public enum Icon { + /// Default square canvas for icon assets embedded inline with 13pt text. + public static let standardCanvas: CGFloat = SpacingTokens.md + /// Visual glyph size inside the standard canvas. + public static let standardGlyph: CGFloat = SpacingTokens.sm + + /// Landing-page recent connection icon canvas with no background tile. + public static let landingRecentCanvas: CGFloat = SpacingTokens.md2 + /// Landing-page recent connection glyph size tuned for card rows. + public static let landingRecentGlyph: CGFloat = SpacingTokens.md2 + + /// Tight 16pt menu canvas matching AppKit/SwiftUI menu row expectations. + public static let menuCanvas: CGFloat = SpacingTokens.md + /// Menu glyph size aligned with the Manage Connections / form-control reference. + public static let menuGlyph: CGFloat = SpacingTokens.md + + /// Form control icon canvas for pop-up buttons and picker labels. + public static let formControlCanvas: CGFloat = SpacingTokens.md + /// Form control glyph size tuned to match Manage Connections type icons. + public static let formControlGlyph: CGFloat = SpacingTokens.md + + /// Sidebar server icon canvas width. + public static let sidebarCanvasWidth: CGFloat = SpacingTokens.md1 + /// Sidebar server icon canvas height. + public static let sidebarCanvasHeight: CGFloat = SpacingTokens.md + /// Sidebar glyph size inside the Tahoe-style sidebar row. + public static let sidebarGlyph: CGFloat = SpacingTokens.sm + } + public enum Form { /// 32pt — Standard minimum height for a settings or property row (compact Tahoe style) public static let rowMinHeight: CGFloat = 32 diff --git a/Echo/Sources/Shared/Notifications/NotificationCategory.swift b/Echo/Sources/Shared/Notifications/NotificationCategory.swift index 8e6c54098..8da137d5b 100644 --- a/Echo/Sources/Shared/Notifications/NotificationCategory.swift +++ b/Echo/Sources/Shared/Notifications/NotificationCategory.swift @@ -58,6 +58,30 @@ enum NotificationCategory: String, CaseIterable, Identifiable, Codable, Sendable var id: String { rawValue } + // MARK: - Critical Default + + /// Whether this category is enabled by default on first launch. + /// Only error and failure notifications are considered critical. + var isCriticalDefault: Bool { + switch self { + case .connectionFailed, + .extensionFailed, + .maintenanceFailed, + .securityToggleFailed, + .indexRebuildFailed, + .databaseCreationFailed, + .databaseSwitchFailed, + .databasePropertiesError, + .jobError, + .generalError: + return true + default: + return false + } + } + + // MARK: - Group + var group: NotificationGroup { switch self { case .connectionConnected, .connectionDisconnected, .connectionFailed: @@ -79,6 +103,8 @@ enum NotificationCategory: String, CaseIterable, Identifiable, Codable, Sendable } } + // MARK: - Display + var displayName: String { switch self { case .connectionConnected: return "Connected" @@ -117,6 +143,46 @@ enum NotificationCategory: String, CaseIterable, Identifiable, Codable, Sendable } } + var displayDescription: String { + switch self { + case .connectionConnected: return "When a database connection is established" + case .connectionDisconnected: return "When a connection is closed or drops" + case .connectionFailed: return "When a connection attempt fails" + case .objectDropped: return "When a database object is deleted" + case .objectRenamed: return "When a database object is renamed" + case .objectCreated: return "When a new database object is created" + case .objectTruncated: return "When a table is truncated" + case .extensionInstalled: return "When a PostgreSQL extension is installed" + case .extensionFailed: return "When an extension operation fails" + case .maintenanceCompleted: return "When a maintenance task finishes" + case .maintenanceFailed: return "When a maintenance task fails" + case .securityDropped: return "When a security object is removed" + case .securityToggleFailed: return "When a security setting change fails" + case .tableStructureUpdated: return "When a table structure change is applied" + case .indexCreated: return "When a new index is created" + case .indexDropped: return "When an index is removed" + case .indexRebuilt: return "When an index rebuild completes" + case .indexRebuildFailed: return "When an index rebuild fails" + case .databaseCreated: return "When a new database is created" + case .databaseCreationFailed: return "When database creation fails" + case .databaseSwitched: return "When the active database changes" + case .databaseSwitchFailed: return "When a database switch fails" + case .databasePropertiesError: return "When database property changes fail" + case .databasePropertiesSaved: return "When database properties are saved" + case .jobStarted: return "When a SQL Agent job starts running" + case .jobStopped: return "When a SQL Agent job is stopped" + case .jobError: return "When a SQL Agent job encounters an error" + case .jobScheduleCreated: return "When a new job schedule is created" + case .jobNotificationSaved: return "When job notification settings are saved" + case .jobPropertiesSaved: return "When job properties are saved" + case .generalSuccess: return "When an operation succeeds" + case .generalError: return "When an unexpected error occurs" + case .generalInfo: return "General informational alerts" + } + } + + // MARK: - Visuals + var defaultIcon: String { switch self { case .connectionConnected: return "checkmark.circle.fill" @@ -195,6 +261,17 @@ enum NotificationGroup: String, CaseIterable, Identifiable, Sendable { } } + var displayDescription: String { + switch self { + case .connection: return "Connection status and errors" + case .objectBrowser: return "Object lifecycle, extensions, and maintenance" + case .tableStructure: return "Schema changes and index operations" + case .database: return "Database creation, switching, and properties" + case .jobs: return "SQL Agent job activity" + case .general: return "App-wide success, error, and info alerts" + } + } + var systemImage: String { switch self { case .connection: return "bolt.horizontal.circle" @@ -209,4 +286,14 @@ enum NotificationGroup: String, CaseIterable, Identifiable, Sendable { var categories: [NotificationCategory] { NotificationCategory.allCases.filter { $0.group == self } } + + /// Whether all categories in this group are critical defaults. + var isAllCritical: Bool { + categories.allSatisfy(\.isCriticalDefault) + } + + /// Whether any categories in this group are critical defaults. + var hasCriticalCategories: Bool { + categories.contains(where: \.isCriticalDefault) + } } diff --git a/Echo/Sources/Shared/Notifications/NotificationPreferences.swift b/Echo/Sources/Shared/Notifications/NotificationPreferences.swift index bba31be92..8e7a46831 100644 --- a/Echo/Sources/Shared/Notifications/NotificationPreferences.swift +++ b/Echo/Sources/Shared/Notifications/NotificationPreferences.swift @@ -13,6 +13,14 @@ enum NotificationDelivery: String, Codable, Hashable, CaseIterable, Sendable { case .both: return "Both" } } + + var displayDescription: String { + switch self { + case .inApp: return "Show banners inside Echo" + case .native: return "Show macOS Notification Center banners" + case .both: return "Show both in-app and macOS banners" + } + } } /// User-configurable notification settings, persisted in ``GlobalSettings``. @@ -20,8 +28,15 @@ struct NotificationPreferences: Codable, Hashable, Sendable { var delivery: NotificationDelivery = .inApp var disabledCategories: Set = [] + /// Whether a specific category is enabled. + /// On first launch (empty `disabledCategories` and no stored defaults), + /// only critical categories (errors and failures) are enabled. func isEnabled(_ category: NotificationCategory) -> Bool { - !disabledCategories.contains(category.rawValue) + if hasExplicitPreferences { + return !disabledCategories.contains(category.rawValue) + } + // Fresh install: only critical categories are on by default + return category.isCriticalDefault } mutating func setEnabled(_ enabled: Bool, for category: NotificationCategory) { @@ -31,4 +46,56 @@ struct NotificationPreferences: Codable, Hashable, Sendable { disabledCategories.insert(category.rawValue) } } + + /// Whether the user has explicitly toggled any category. + /// When false, the system uses the critical-defaults policy. + private var hasExplicitPreferences: Bool { + // If the disabled set is non-empty, the user has made choices. + // We also check for a sentinel key written on first explicit toggle. + return disabledCategories.contains(hasExplicitPreferencesKey) + || disabledCategories.contains(allEnabledSentinelKey) + } + + /// Mark that the user has explicitly chosen their preferences. + mutating func markExplicitPreferences() { + // Ensure the preferences are recognized as explicit going forward + if !hasExplicitPreferences { + disabledCategories.insert(hasExplicitPreferencesKey) + } + } + + /// Enable all notification categories. + mutating func enableAll() { + disabledCategories = [hasExplicitPreferencesKey] + } + + /// Disable all notification categories. + mutating func disableAll() { + var all = Set(NotificationCategory.allCases.map(\.rawValue)) + all.insert(hasExplicitPreferencesKey) + disabledCategories = all + } + + /// Whether all categories are enabled (given explicit preferences). + var isAllEnabled: Bool { + guard hasExplicitPreferences else { return false } + return disabledCategories.subtracting([hasExplicitPreferencesKey, allEnabledSentinelKey]).isEmpty + } + + /// Whether a whole group has any enabled notifications. + func isGroupEnabled(_ group: NotificationGroup) -> Bool { + group.categories.contains { isEnabled($0) } + } + + /// Enable or disable an entire group at once. + mutating func setGroupEnabled(_ enabled: Bool, for group: NotificationGroup) { + markExplicitPreferences() + for category in group.categories { + setEnabled(enabled, for: category) + } + } + + // Sentinel keys for tracking explicit user preferences + private var hasExplicitPreferencesKey: String { "__explicit" } + private var allEnabledSentinelKey: String { "__allEnabled" } } diff --git a/Echo/Sources/Shared/PlatformBridge/NSImage+DatabaseIcons.swift b/Echo/Sources/Shared/PlatformBridge/NSImage+DatabaseIcons.swift new file mode 100644 index 000000000..07b24b74f --- /dev/null +++ b/Echo/Sources/Shared/PlatformBridge/NSImage+DatabaseIcons.swift @@ -0,0 +1,48 @@ +import AppKit + +extension DatabaseType { + func rasterSymbolImage( + canvasSize: CGFloat, + glyphSize: CGFloat + ) -> NSImage? { + guard let image = NSImage(named: iconName) else { return nil } + + let targetSize = NSSize(width: canvasSize, height: canvasSize) + let canvas = NSImage(size: targetSize, flipped: false) { rect in + let sourceSize = image.size + guard sourceSize.width > 0, sourceSize.height > 0 else { return false } + + let scale = min(glyphSize / sourceSize.width, glyphSize / sourceSize.height) + let drawSize = NSSize( + width: sourceSize.width * scale, + height: sourceSize.height * scale + ) + let drawRect = NSRect( + x: rect.midX - drawSize.width / 2, + y: rect.midY - drawSize.height / 2, + width: drawSize.width, + height: drawSize.height + ) + + image.draw(in: drawRect, from: .zero, operation: .sourceOver, fraction: 1) + return true + } + + canvas.isTemplate = usesTemplateIcon + return canvas + } + + func menuIconImage( + canvasSize: CGFloat = LayoutTokens.Icon.menuCanvas, + glyphSize: CGFloat = LayoutTokens.Icon.menuGlyph + ) -> NSImage? { + rasterSymbolImage(canvasSize: canvasSize, glyphSize: glyphSize) + } + + func formControlIconImage() -> NSImage? { + rasterSymbolImage( + canvasSize: LayoutTokens.Icon.formControlCanvas, + glyphSize: LayoutTokens.Icon.formControlGlyph + ) + } +} diff --git a/Echo/Sources/UI/Modals/FolderEditorSheet+Components.swift b/Echo/Sources/UI/Modals/FolderEditorSheet+Components.swift new file mode 100644 index 000000000..4e29e74ec --- /dev/null +++ b/Echo/Sources/UI/Modals/FolderEditorSheet+Components.swift @@ -0,0 +1,60 @@ +import SwiftUI + +extension FolderEditorSheet { + + // MARK: - Icon Palette + + var iconPaletteView: some View { + HStack(spacing: SpacingTokens.xxs2) { + ForEach(availableIcons, id: \.self) { iconName in + iconSwatch(name: iconName, isSelected: selectedIcon == iconName) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.15)) { + selectedIcon = iconName + } + } + } + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + + func iconSwatch(name: String, isSelected: Bool) -> some View { + Image(systemName: name) + .font(TypographyTokens.prominent) + .frame(width: 26, height: 26) + .foregroundStyle(isSelected ? Color.white : ColorTokens.Text.secondary) + .background(isSelected ? ColorTokens.accent : Color.clear, in: RoundedRectangle(cornerRadius: 6)) + .contentShape(Rectangle()) + } + + // MARK: - Color Palette + + var colorPaletteView: some View { + HStack(spacing: SpacingTokens.xs) { + ForEach(FolderIdentityPalette.defaults, id: \.self) { hex in + let swatch = Color(hex: hex) ?? ColorTokens.accent + colorSwatch(color: swatch, isSelected: selectedColorHex == hex) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.15)) { selectedColorHex = hex } + } + } + + ColorPicker("", selection: folderColorBinding, supportsOpacity: false) + .labelsHidden() + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + + func colorSwatch(color: Color, isSelected: Bool) -> some View { + Circle().fill(color).frame(width: SpacingTokens.md2, height: SpacingTokens.md2) + .overlay { + if isSelected { + Image(systemName: "checkmark") + .font(TypographyTokens.label.weight(.bold)) + .foregroundStyle(.white) + } + } + .overlay(Circle().strokeBorder(ColorTokens.Text.primary.opacity(0.15), lineWidth: 0.5)) + .contentShape(Circle()) + } +} diff --git a/Echo/Sources/UI/Modals/FolderEditorSheet+Credentials.swift b/Echo/Sources/UI/Modals/FolderEditorSheet+Credentials.swift new file mode 100644 index 000000000..6384358fb --- /dev/null +++ b/Echo/Sources/UI/Modals/FolderEditorSheet+Credentials.swift @@ -0,0 +1,104 @@ +import SwiftUI + +extension FolderEditorSheet { + + // MARK: - Credentials + + @ViewBuilder + var credentialsFormSection: some View { + Section("Credentials") { + PropertyRow(title: "Mode") { + Picker("", selection: $credentialMode) { + Text("None").tag(FolderCredentialMode.none) + Text("Manual").tag(FolderCredentialMode.manual) + Text("Identity").tag(FolderCredentialMode.identity) + if canUseInheritance { Text("Inherit").tag(FolderCredentialMode.inherit) } + } + .labelsHidden() + .pickerStyle(.segmented) + } + + switch credentialMode { + case .manual: + PropertyRow(title: "Username") { + TextField("", text: $manualUsername, prompt: Text("shared_user")) + .textFieldStyle(.plain) + .multilineTextAlignment(.trailing) + } + + PropertyRow(title: "Password") { + SecureField("", text: Binding( + get: { manualPassword }, + set: { manualPassword = $0; manualPasswordDirty = true } + ), prompt: Text("••••••••")) + .textFieldStyle(.plain) + .multilineTextAlignment(.trailing) + } + + if editingFolderUsesManual && !manualPasswordDirty { + Text("Existing password will be kept unless changed.") + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Text.secondary) + .listRowSeparator(.hidden) + } + case .identity: + identitySelectionContent + case .inherit: + if let identity = inheritedIdentity { + Text("Inherits identity \"\(identity.name)\" from parent folder.") + .foregroundStyle(ColorTokens.Text.secondary) + .font(TypographyTokens.formDescription) + .listRowSeparator(.hidden) + } else { + Text("Parent folder does not provide credentials.") + .foregroundStyle(ColorTokens.Status.error) + .font(TypographyTokens.formDescription) + .listRowSeparator(.hidden) + } + case .none: + EmptyView() + } + } + } + + var identitySelectionContent: some View { + Group { + if availableIdentities.isEmpty { + PropertyRow(title: "Identity") { + VStack(alignment: .trailing, spacing: SpacingTokens.xs) { + Text("No identities available.") + .foregroundStyle(ColorTokens.Text.secondary) + .font(TypographyTokens.formDescription) + Button("Create Identity") { + identityEditorState = .create(parent: nil, token: UUID()) + } + .buttonStyle(.bordered) + .controlSize(.small) + } + } + } else { + PropertyRow(title: "Identity") { + HStack(spacing: SpacingTokens.xs) { + Picker("", selection: $selectedIdentityID) { + Text("Select").tag(UUID?.none) + ForEach(availableIdentities, id: \.id) { + Text($0.name).tag(UUID?.some($0.id)) + } + } + .labelsHidden() + .pickerStyle(.menu) + + Button { + identityEditorState = .create(parent: nil, token: UUID()) + } label: { + Image(systemName: "plus") + } + .buttonStyle(.bordered) + .controlSize(.small) + .accessibilityLabel("Add identity") + } + } + } + } + } +} diff --git a/Echo/Sources/UI/Modals/FolderEditorSheet+Logic.swift b/Echo/Sources/UI/Modals/FolderEditorSheet+Logic.swift new file mode 100644 index 000000000..9df34d912 --- /dev/null +++ b/Echo/Sources/UI/Modals/FolderEditorSheet+Logic.swift @@ -0,0 +1,83 @@ +import SwiftUI + +extension FolderEditorSheet { + + // MARK: - Logic + + func handleCredentialModeChange(_ newMode: FolderCredentialMode) { + if newMode == .manual { + manualUsername = editingFolderUsesManual ? (editingFolder?.manualUsername ?? "") : "" + manualPassword = "" + manualPasswordDirty = false + } else if newMode == .identity && selectedIdentityID == nil { + selectedIdentityID = availableIdentities.first?.id + } else { + manualUsername = "" + manualPassword = "" + manualPasswordDirty = false + } + } + + func prepareInitialValues() { + if case .edit(let folder) = state { + name = folder.name + folderDescription = folder.folderDescription ?? "" + selectedColorHex = folder.colorHex + selectedIcon = folder.icon + selectedKind = folder.kind + selectedParentID = folder.parentFolderID + credentialMode = folder.credentialMode + selectedIdentityID = folder.identityID + manualUsername = folder.manualUsername ?? "" + manualPassword = "" + manualPasswordDirty = false + } else if case .create(let kind, let parent, _) = state { + name = "" + folderDescription = "" + selectedColorHex = FolderIdentityPalette.defaults.first ?? "5A9CDE" + selectedIcon = SavedFolder.defaultIcon + selectedKind = kind + selectedParentID = parent?.id + credentialMode = .none + selectedIdentityID = nil + if let parent { + selectedColorHex = parent.colorHex + if parent.credentialMode == .inherit { credentialMode = .inherit } + } + manualUsername = "" + manualPassword = "" + manualPasswordDirty = false + } + } + + func saveFolder() async { + var folder: SavedFolder + switch state { + case .create: + folder = SavedFolder(name: name) + folder.id = UUID() + folder.projectID = projectStore.selectedProject?.id + case .edit(let existing): + folder = existing + folder.name = name + } + + let trimmedDescription = folderDescription.trimmingCharacters(in: .whitespacesAndNewlines) + folder.folderDescription = trimmedDescription.isEmpty ? nil : trimmedDescription + folder.icon = selectedIcon + folder.colorHex = selectedColorHex + folder.kind = selectedKind + folder.parentFolderID = selectedParentID + folder.credentialMode = isIdentityFolder ? .none : credentialMode + folder.identityID = credentialMode == .identity && !isIdentityFolder ? selectedIdentityID : nil + folder.manualUsername = credentialMode == .manual && !isIdentityFolder ? manualUsername.trimmingCharacters(in: .whitespacesAndNewlines) : nil + + let pw = (credentialMode == .manual && !isIdentityFolder && manualPasswordDirty) ? manualPassword.trimmingCharacters(in: .whitespacesAndNewlines) : nil + if let pw { try? environmentState.identityRepository.setPassword(pw, for: &folder) } + + let isNew = !isEditing + try? await connectionStore.updateFolder(folder) + if isNew && folder.kind == .connections { connectionStore.selectedFolderID = folder.id } + dismiss() + } +} diff --git a/Echo/Sources/UI/Modals/FolderEditorSheet+Sections.swift b/Echo/Sources/UI/Modals/FolderEditorSheet+Sections.swift new file mode 100644 index 000000000..f67a13b2d --- /dev/null +++ b/Echo/Sources/UI/Modals/FolderEditorSheet+Sections.swift @@ -0,0 +1,74 @@ +import SwiftUI + +extension FolderEditorSheet { + + // MARK: - Form + + var formContent: some View { + Form { + Section { + PropertyRow(title: "Name") { + TextField("", text: $name, prompt: Text("Folder name")) + .textFieldStyle(.plain) + .multilineTextAlignment(.trailing) + } + + if hasDuplicateName { + Text("A folder with this name already exists here.") + .font(TypographyTokens.formDescription) + .foregroundStyle(ColorTokens.Status.error) + .listRowSeparator(.hidden) + } + + PropertyRow(title: "Description") { + TextField("", text: $folderDescription, prompt: Text("Optional"), axis: .vertical) + .textFieldStyle(.plain) + .lineLimit(1...3) + .multilineTextAlignment(.trailing) + } + + PropertyRow(title: "Icon") { iconPaletteView } + PropertyRow(title: "Color") { colorPaletteView } + } header: { + Text(isEditing ? "Edit Folder" : "New Folder") + } + + Section("Location") { + PropertyRow(title: "Type") { + Picker("", selection: $selectedKind) { + Text("Connections").tag(FolderKind.connections) + Text("Identities").tag(FolderKind.identities) + } + .labelsHidden() + .pickerStyle(.menu) + } + + PropertyRow(title: "Parent") { + Picker("", selection: $selectedParentID) { + Text("None").tag(UUID?.none) + ForEach(hierarchicalParentFolders, id: \.folder.id) { item in + Text(item.path).tag(UUID?.some(item.folder.id)) + } + } + .labelsHidden() + .pickerStyle(.menu) + } + } + + if !isIdentityFolder { + credentialsFormSection + } + } + .formStyle(.grouped) + .scrollContentBackground(.hidden) + .scrollDisabled(true) + .onChange(of: credentialMode) { _, newMode in handleCredentialModeChange(newMode) } + .onChange(of: selectedKind) { _, _ in + selectedParentID = nil + selectedIcon = SavedFolder.defaultIcon + if selectedKind == .identities { + credentialMode = .none + } + } + } +} diff --git a/Echo/Sources/UI/Modals/FolderEditorSheet.swift b/Echo/Sources/UI/Modals/FolderEditorSheet.swift index fbdba761a..e6b111823 100644 --- a/Echo/Sources/UI/Modals/FolderEditorSheet.swift +++ b/Echo/Sources/UI/Modals/FolderEditorSheet.swift @@ -1,52 +1,52 @@ import SwiftUI struct FolderEditorSheet: View { - @Environment(ProjectStore.self) private var projectStore - @Environment(ConnectionStore.self) private var connectionStore - @Environment(EnvironmentState.self) private var environmentState - @Environment(\.dismiss) private var dismiss + @Environment(ProjectStore.self) var projectStore + @Environment(ConnectionStore.self) var connectionStore + @Environment(EnvironmentState.self) var environmentState + @Environment(\.dismiss) var dismiss let state: FolderEditorState - @State private var name: String = "" - @State private var folderDescription: String = "" - @State private var selectedColorHex: String = FolderIdentityPalette.defaults.first ?? "5A9CDE" - @State private var selectedIcon: String = SavedFolder.defaultIcon - @State private var selectedKind: FolderKind = .connections - @State private var selectedParentID: UUID? - @State private var credentialMode: FolderCredentialMode = .none - @State private var selectedIdentityID: UUID? - @State private var manualUsername: String = "" - @State private var manualPassword: String = "" - @State private var manualPasswordDirty = false - @State private var identityEditorState: IdentityEditorState? - - private var editingFolder: SavedFolder? { + @State var name: String = "" + @State var folderDescription: String = "" + @State var selectedColorHex: String = FolderIdentityPalette.defaults.first ?? "5A9CDE" + @State var selectedIcon: String = SavedFolder.defaultIcon + @State var selectedKind: FolderKind = .connections + @State var selectedParentID: UUID? + @State var credentialMode: FolderCredentialMode = .none + @State var selectedIdentityID: UUID? + @State var manualUsername: String = "" + @State var manualPassword: String = "" + @State var manualPasswordDirty = false + @State var identityEditorState: IdentityEditorState? + + var editingFolder: SavedFolder? { if case .edit(let folder) = state { return folder } return nil } - private var isEditing: Bool { editingFolder != nil } + var isEditing: Bool { editingFolder != nil } - private var isIdentityFolder: Bool { selectedKind == .identities } + var isIdentityFolder: Bool { selectedKind == .identities } - private var selectedParentFolder: SavedFolder? { + var selectedParentFolder: SavedFolder? { guard let id = selectedParentID else { return nil } return connectionStore.folders.first(where: { $0.id == id }) } - private var inheritedIdentity: SavedIdentity? { + var inheritedIdentity: SavedIdentity? { guard let parent = selectedParentFolder else { return nil } return environmentState.identityRepository.resolveInheritedIdentity(folderID: parent.id) } - private var editingFolderUsesManual: Bool { editingFolder?.credentialMode == .manual } + var editingFolderUsesManual: Bool { editingFolder?.credentialMode == .manual } - private var availableIdentities: [SavedIdentity] { + var availableIdentities: [SavedIdentity] { connectionStore.identities.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } - private var availableParentFolders: [SavedFolder] { + var availableParentFolders: [SavedFolder] { let projectID = projectStore.selectedProject?.id let editingID = editingFolder?.id return connectionStore.folders @@ -54,7 +54,7 @@ struct FolderEditorSheet: View { .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } - private func folderPath(for folder: SavedFolder) -> String { + func folderPath(for folder: SavedFolder) -> String { var components: [String] = [folder.name] var current = folder while let parentID = current.parentFolderID, @@ -65,26 +65,26 @@ struct FolderEditorSheet: View { return components.joined(separator: " / ") } - private var hierarchicalParentFolders: [(folder: SavedFolder, path: String)] { + var hierarchicalParentFolders: [(folder: SavedFolder, path: String)] { availableParentFolders.map { folder in (folder: folder, path: folderPath(for: folder)) } .sorted { $0.path.localizedCaseInsensitiveCompare($1.path) == .orderedAscending } } - private var folderColorBinding: Binding { + var folderColorBinding: Binding { Binding( get: { Color(hex: selectedColorHex) ?? ColorTokens.accent }, set: { color in selectedColorHex = color.toHex() ?? selectedColorHex } ) } - private var canUseInheritance: Bool { + var canUseInheritance: Bool { guard let parent = selectedParentFolder else { return false } return parent.credentialMode != .none } - private var hasDuplicateName: Bool { + var hasDuplicateName: Bool { let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedName.isEmpty else { return false } let projectID = projectStore.selectedProject?.id @@ -100,7 +100,7 @@ struct FolderEditorSheet: View { } } - private var isValid: Bool { + var isValid: Bool { let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) if trimmedName.isEmpty { return false } if hasDuplicateName { return false } @@ -114,7 +114,7 @@ struct FolderEditorSheet: View { } } - private var availableIcons: [String] { + var availableIcons: [String] { isIdentityFolder ? FolderIdentityPalette.identityIcons : FolderIdentityPalette.connectionIcons } @@ -149,309 +149,4 @@ struct FolderEditorSheet: View { } .onAppear(perform: prepareInitialValues) } - - // MARK: - Form - - private var formContent: some View { - Form { - Section { - PropertyRow(title: "Name") { - TextField("", text: $name, prompt: Text("Folder name")) - .textFieldStyle(.plain) - .multilineTextAlignment(.trailing) - } - - if hasDuplicateName { - Text("A folder with this name already exists here.") - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Status.error) - .listRowSeparator(.hidden) - } - - PropertyRow(title: "Description") { - TextField("", text: $folderDescription, prompt: Text("Optional"), axis: .vertical) - .textFieldStyle(.plain) - .lineLimit(1...3) - .multilineTextAlignment(.trailing) - } - - PropertyRow(title: "Icon") { iconPaletteView } - PropertyRow(title: "Color") { colorPaletteView } - } header: { - Text(isEditing ? "Edit Folder" : "New Folder") - } - - Section("Location") { - PropertyRow(title: "Type") { - Picker("", selection: $selectedKind) { - Text("Connections").tag(FolderKind.connections) - Text("Identities").tag(FolderKind.identities) - } - .labelsHidden() - .pickerStyle(.menu) - } - - PropertyRow(title: "Parent") { - Picker("", selection: $selectedParentID) { - Text("None").tag(UUID?.none) - ForEach(hierarchicalParentFolders, id: \.folder.id) { item in - Text(item.path).tag(UUID?.some(item.folder.id)) - } - } - .labelsHidden() - .pickerStyle(.menu) - } - } - - if !isIdentityFolder { - credentialsFormSection - } - } - .formStyle(.grouped) - .scrollContentBackground(.hidden) - .scrollDisabled(true) - .onChange(of: credentialMode) { _, newMode in handleCredentialModeChange(newMode) } - .onChange(of: selectedKind) { _, _ in - selectedParentID = nil - selectedIcon = SavedFolder.defaultIcon - if selectedKind == .identities { - credentialMode = .none - } - } - } - - // MARK: - Icon Palette - - private var iconPaletteView: some View { - HStack(spacing: SpacingTokens.xxs2) { - ForEach(availableIcons, id: \.self) { iconName in - iconSwatch(name: iconName, isSelected: selectedIcon == iconName) - .onTapGesture { - withAnimation(.easeInOut(duration: 0.15)) { - selectedIcon = iconName - } - } - } - } - .frame(maxWidth: .infinity, alignment: .trailing) - } - - private func iconSwatch(name: String, isSelected: Bool) -> some View { - Image(systemName: name) - .font(TypographyTokens.prominent) - .frame(width: 26, height: 26) - .foregroundStyle(isSelected ? Color.white : ColorTokens.Text.secondary) - .background(isSelected ? ColorTokens.accent : Color.clear, in: RoundedRectangle(cornerRadius: 6)) - .contentShape(Rectangle()) - } - - // MARK: - Color Palette - - private var colorPaletteView: some View { - HStack(spacing: SpacingTokens.xs) { - ForEach(FolderIdentityPalette.defaults, id: \.self) { hex in - let swatch = Color(hex: hex) ?? ColorTokens.accent - colorSwatch(color: swatch, isSelected: selectedColorHex == hex) - .onTapGesture { - withAnimation(.easeInOut(duration: 0.15)) { selectedColorHex = hex } - } - } - - ColorPicker("", selection: folderColorBinding, supportsOpacity: false) - .labelsHidden() - } - .frame(maxWidth: .infinity, alignment: .trailing) - } - - private func colorSwatch(color: Color, isSelected: Bool) -> some View { - Circle().fill(color).frame(width: SpacingTokens.md2, height: SpacingTokens.md2) - .overlay { - if isSelected { - Image(systemName: "checkmark") - .font(TypographyTokens.label.weight(.bold)) - .foregroundStyle(Color.white) - } - } - .overlay(Circle().strokeBorder(ColorTokens.Text.primary.opacity(0.15), lineWidth: 0.5)) - .contentShape(Circle()) - } - - // MARK: - Credentials - - @ViewBuilder - private var credentialsFormSection: some View { - Section("Credentials") { - PropertyRow(title: "Mode") { - Picker("", selection: $credentialMode) { - Text("None").tag(FolderCredentialMode.none) - Text("Manual").tag(FolderCredentialMode.manual) - Text("Identity").tag(FolderCredentialMode.identity) - if canUseInheritance { Text("Inherit").tag(FolderCredentialMode.inherit) } - } - .labelsHidden() - .pickerStyle(.segmented) - } - - switch credentialMode { - case .manual: - PropertyRow(title: "Username") { - TextField("", text: $manualUsername, prompt: Text("shared_user")) - .textFieldStyle(.plain) - .multilineTextAlignment(.trailing) - } - - PropertyRow(title: "Password") { - SecureField("", text: Binding( - get: { manualPassword }, - set: { manualPassword = $0; manualPasswordDirty = true } - ), prompt: Text("••••••••")) - .textFieldStyle(.plain) - .multilineTextAlignment(.trailing) - } - - if editingFolderUsesManual && !manualPasswordDirty { - Text("Existing password will be kept unless changed.") - .font(TypographyTokens.formDescription) - .foregroundStyle(ColorTokens.Text.secondary) - .listRowSeparator(.hidden) - } - case .identity: - identitySelectionContent - case .inherit: - if let identity = inheritedIdentity { - Text("Inherits identity \"\(identity.name)\" from parent folder.") - .foregroundStyle(ColorTokens.Text.secondary) - .font(TypographyTokens.formDescription) - .listRowSeparator(.hidden) - } else { - Text("Parent folder does not provide credentials.") - .foregroundStyle(ColorTokens.Status.error) - .font(TypographyTokens.formDescription) - .listRowSeparator(.hidden) - } - case .none: - EmptyView() - } - } - } - - private var identitySelectionContent: some View { - Group { - if availableIdentities.isEmpty { - PropertyRow(title: "Identity") { - VStack(alignment: .trailing, spacing: SpacingTokens.xs) { - Text("No identities available.") - .foregroundStyle(ColorTokens.Text.secondary) - .font(TypographyTokens.formDescription) - Button("Create Identity") { - identityEditorState = .create(parent: nil, token: UUID()) - } - .buttonStyle(.bordered) - .controlSize(.small) - } - } - } else { - PropertyRow(title: "Identity") { - HStack(spacing: SpacingTokens.xs) { - Picker("", selection: $selectedIdentityID) { - Text("Select").tag(UUID?.none) - ForEach(availableIdentities, id: \.id) { - Text($0.name).tag(UUID?.some($0.id)) - } - } - .labelsHidden() - .pickerStyle(.menu) - - Button { - identityEditorState = .create(parent: nil, token: UUID()) - } label: { - Image(systemName: "plus") - } - .buttonStyle(.bordered) - .controlSize(.small) - .accessibilityLabel("Add identity") - } - } - } - } - } - - // MARK: - Logic - - private func handleCredentialModeChange(_ newMode: FolderCredentialMode) { - if newMode == .manual { - manualUsername = editingFolderUsesManual ? (editingFolder?.manualUsername ?? "") : "" - manualPassword = "" - manualPasswordDirty = false - } else if newMode == .identity && selectedIdentityID == nil { - selectedIdentityID = availableIdentities.first?.id - } else { - manualUsername = "" - manualPassword = "" - manualPasswordDirty = false - } - } - - private func prepareInitialValues() { - if case .edit(let folder) = state { - name = folder.name - folderDescription = folder.folderDescription ?? "" - selectedColorHex = folder.colorHex - selectedIcon = folder.icon - selectedKind = folder.kind - selectedParentID = folder.parentFolderID - credentialMode = folder.credentialMode - selectedIdentityID = folder.identityID - manualUsername = folder.manualUsername ?? "" - manualPassword = "" - manualPasswordDirty = false - } else if case .create(let kind, let parent, _) = state { - name = "" - folderDescription = "" - selectedColorHex = FolderIdentityPalette.defaults.first ?? "5A9CDE" - selectedIcon = SavedFolder.defaultIcon - selectedKind = kind - selectedParentID = parent?.id - credentialMode = .none - selectedIdentityID = nil - if let parent { - selectedColorHex = parent.colorHex - if parent.credentialMode == .inherit { credentialMode = .inherit } - } - manualUsername = "" - manualPassword = "" - manualPasswordDirty = false - } - } - - private func saveFolder() async { - var folder: SavedFolder - switch state { - case .create: - folder = SavedFolder(name: name) - folder.id = UUID() - folder.projectID = projectStore.selectedProject?.id - case .edit(let existing): - folder = existing - folder.name = name - } - - let trimmedDescription = folderDescription.trimmingCharacters(in: .whitespacesAndNewlines) - folder.folderDescription = trimmedDescription.isEmpty ? nil : trimmedDescription - folder.icon = selectedIcon - folder.colorHex = selectedColorHex - folder.kind = selectedKind - folder.parentFolderID = selectedParentID - folder.credentialMode = isIdentityFolder ? .none : credentialMode - folder.identityID = credentialMode == .identity && !isIdentityFolder ? selectedIdentityID : nil - folder.manualUsername = credentialMode == .manual && !isIdentityFolder ? manualUsername.trimmingCharacters(in: .whitespacesAndNewlines) : nil - - let pw = (credentialMode == .manual && !isIdentityFolder && manualPasswordDirty) ? manualPassword.trimmingCharacters(in: .whitespacesAndNewlines) : nil - if let pw { try? environmentState.identityRepository.setPassword(pw, for: &folder) } - - let isNew = !isEditing - try? await connectionStore.updateFolder(folder) - if isNew && folder.kind == .connections { connectionStore.selectedFolderID = folder.id } - dismiss() - } } diff --git a/EchoTests/Components/MSSQLDataTypePickerTests.swift b/EchoTests/Components/MSSQLDataTypePickerTests.swift new file mode 100644 index 000000000..60621eb4c --- /dev/null +++ b/EchoTests/Components/MSSQLDataTypePickerTests.swift @@ -0,0 +1,22 @@ +import Testing +@testable import Echo + +@Suite("MSSQLDataTypePicker") +struct MSSQLDataTypePickerTests { + + @Test func preservesBareUnicodeTypeWithoutInjectingDefaultLength() { + let state = MSSQLDataTypePicker.selectionState(for: "nvarchar") + + #expect(state.baseType == "nvarchar") + #expect(state.sizeParam.isEmpty) + #expect(state.isCustom == false) + } + + @Test func preservesExplicitLengthForParameterizedType() { + let state = MSSQLDataTypePicker.selectionState(for: "nvarchar(4000)") + + #expect(state.baseType == "nvarchar") + #expect(state.sizeParam == "4000") + #expect(state.isCustom == false) + } +} diff --git a/EchoTests/Models/GlobalSettingsExtendedTests.swift b/EchoTests/Models/GlobalSettingsExtendedTests.swift index 1e765d26f..a29109e21 100644 --- a/EchoTests/Models/GlobalSettingsExtendedTests.swift +++ b/EchoTests/Models/GlobalSettingsExtendedTests.swift @@ -36,28 +36,6 @@ struct GlobalSettingsExtendedTests { } } - // MARK: - NativePsqlRuntimePreference - - @Test func nativePsqlRuntimePreferenceAllCases() { - let cases = NativePsqlRuntimePreference.allCases - #expect(cases.count == 2) - #expect(cases.contains(.bundled)) - #expect(cases.contains(.system)) - } - - @Test func nativePsqlRuntimePreferenceDisplayNames() { - #expect(NativePsqlRuntimePreference.bundled.displayName == "Bundled Binary") - #expect(NativePsqlRuntimePreference.system.displayName == "System Binary") - } - - @Test func nativePsqlRuntimePreferenceCodableRoundTrip() throws { - for pref in NativePsqlRuntimePreference.allCases { - let data = try JSONEncoder().encode(pref) - let decoded = try JSONDecoder().decode(NativePsqlRuntimePreference.self, from: data) - #expect(decoded == pref) - } - } - // MARK: - SidebarAutoExpandSection: displayName @Test func sidebarAutoExpandSectionDisplayNames() { @@ -213,6 +191,57 @@ struct GlobalSettingsExtendedTests { #expect(overrides.textHex == nil) } + @Test func globalSettingsObjectBrowserCacheDefault() { + let settings = GlobalSettings() + #expect(settings.objectBrowserCacheMaxBytes == 512 * 1_024 * 1_024) + } + + @Test func globalSettingsObjectBrowserCacheCodableRoundTrip() throws { + var settings = GlobalSettings() + settings.objectBrowserCacheMaxBytes = 256 * 1_024 * 1_024 + + let data = try JSONEncoder().encode(settings) + let decoded = try JSONDecoder().decode(GlobalSettings.self, from: data) + + #expect(decoded.objectBrowserCacheMaxBytes == 256 * 1_024 * 1_024) + } + + // MARK: - NotificationPreferences + + @Test func notificationGroupRemainsEnabledWhenAnyCategoryIsEnabled() { + var preferences = NotificationPreferences() + preferences.enableAll() + preferences.setEnabled(false, for: .connectionConnected) + + #expect(preferences.isGroupEnabled(.connection)) + #expect(!preferences.isEnabled(.connectionConnected)) + #expect(preferences.isEnabled(.connectionDisconnected)) + #expect(preferences.isEnabled(.connectionFailed)) + } + + @Test func notificationGroupToggleDisablesAllCategoriesInGroup() { + var preferences = NotificationPreferences() + preferences.enableAll() + preferences.setGroupEnabled(false, for: .connection) + + #expect(!preferences.isGroupEnabled(.connection)) + #expect(!preferences.isEnabled(.connectionConnected)) + #expect(!preferences.isEnabled(.connectionDisconnected)) + #expect(!preferences.isEnabled(.connectionFailed)) + #expect(preferences.isEnabled(.generalSuccess)) + } + + @Test func notificationExplicitPreferencesPreserveSingleCategoryChanges() { + var preferences = NotificationPreferences() + preferences.markExplicitPreferences() + preferences.setEnabled(true, for: .generalInfo) + preferences.setEnabled(false, for: .generalSuccess) + + #expect(preferences.isGroupEnabled(.general)) + #expect(preferences.isEnabled(.generalInfo)) + #expect(!preferences.isEnabled(.generalSuccess)) + } + // MARK: - GlobalSettings: Codable round-trip @Test func globalSettingsCodableRoundTripPreservesAllFields() throws { @@ -240,11 +269,6 @@ struct GlobalSettingsExtendedTests { settings.sidebarAutoExpandSections = [.databases, .tables] settings.sidebarAutoExpandPostgresql = [.databases, .materializedViews] settings.sidebarAutoExpandSQLServer = [.databases, .procedures] - settings.nativePsqlEnabled = true - settings.nativePsqlRuntimePreference = .system - settings.nativePsqlAllowSystemBinaryFallback = true - settings.nativePsqlAllowShellEscape = false - settings.nativePsqlAllowFileCommands = false settings.sidebarIconColorMode = .monochrome settings.managedPostgresConsoleEnabled = false @@ -274,11 +298,6 @@ struct GlobalSettingsExtendedTests { #expect(decoded.sidebarAutoExpandSections == [.databases, .tables]) #expect(decoded.sidebarAutoExpandPostgresql == [.databases, .materializedViews]) #expect(decoded.sidebarAutoExpandSQLServer == [.databases, .procedures]) - #expect(decoded.nativePsqlEnabled == true) - #expect(decoded.nativePsqlRuntimePreference == .system) - #expect(decoded.nativePsqlAllowSystemBinaryFallback == true) - #expect(decoded.nativePsqlAllowShellEscape == false) - #expect(decoded.nativePsqlAllowFileCommands == false) #expect(decoded.sidebarIconColorMode == .monochrome) #expect(decoded.managedPostgresConsoleEnabled == false) } diff --git a/EchoTests/Models/TableStructureEditorModelsTests.swift b/EchoTests/Models/TableStructureEditorModelsTests.swift index e99e04f76..794e7eaef 100644 --- a/EchoTests/Models/TableStructureEditorModelsTests.swift +++ b/EchoTests/Models/TableStructureEditorModelsTests.swift @@ -208,6 +208,12 @@ struct TableStructureEditorModelsTests { #expect(idx.isDirty == false) } + @Test func indexModelUnchangedIsNotDirtyForMSSQLDefaultType() { + let snapshot = IndexModel.Snapshot(name: "idx_test", columns: [], isUnique: false, filterCondition: nil, indexType: "nonclustered") + let idx = IndexModel(original: snapshot, name: "idx_test", columns: [], isUnique: false, filterCondition: "", indexType: "nonclustered") + #expect(idx.isDirty == false) + } + @Test func indexModelNameChangeIsDirty() { let snapshot = IndexModel.Snapshot(name: "idx_old", columns: [], isUnique: false, filterCondition: nil) let idx = IndexModel(original: snapshot, name: "idx_new", columns: [], isUnique: false, filterCondition: "") diff --git a/EchoTests/Services/AppleSignInCoordinatorTests.swift b/EchoTests/Services/AppleSignInCoordinatorTests.swift new file mode 100644 index 000000000..fe08d89ed --- /dev/null +++ b/EchoTests/Services/AppleSignInCoordinatorTests.swift @@ -0,0 +1,31 @@ +import AuthenticationServices +import Foundation +import Testing +@testable import Echo + +@Suite("AppleSignInCoordinator") +struct AppleSignInCoordinatorTests { + + @Test func mapAuthErrorReturnsCancelledForCanceledAuthorization() { + let error = ASAuthorizationError(.canceled) + + let mapped = AppleSignInCoordinator.mapAuthError(error) + + #expect(mapped == .cancelled) + } + + @Test func mapAuthErrorReturnsUnknownForOtherErrors() { + let error = NSError(domain: "AppleSignInCoordinatorTests", code: 42, userInfo: [ + NSLocalizedDescriptionKey: "Unexpected failure" + ]) + + let mapped = AppleSignInCoordinator.mapAuthError(error) + + switch mapped { + case .unknown(let message): + #expect(message == "Unexpected failure") + default: + Issue.record("Expected unknown auth error, got \(mapped)") + } + } +} diff --git a/EchoTests/Services/ObjectBrowserCacheStoreTests.swift b/EchoTests/Services/ObjectBrowserCacheStoreTests.swift new file mode 100644 index 000000000..5ae758bab --- /dev/null +++ b/EchoTests/Services/ObjectBrowserCacheStoreTests.swift @@ -0,0 +1,102 @@ +import Foundation +import Testing +@testable import Echo + +@Suite("Object Browser Cache Store") +struct ObjectBrowserCacheStoreTests { + @Test func ignoresEntryWhenConnectionFingerprintChanges() async throws { + let store = ObjectBrowserCacheStore(configuration: .init(rootDirectory: try makeTempDirectory())) + let structure = TestFixtures.databaseStructure(databaseCount: 1, schemasPerDatabase: 1, tablesPerSchema: 1) + let original = SavedConnection( + id: UUID(), + connectionName: "Demo", + host: "db.local", + port: 5432, + database: "analytics", + username: "echo", + databaseType: .postgresql + ) + + try await store.stashStructure(structure, for: original, limitBytes: 512 * 1_024 * 1_024) + + var changed = original + changed.port = 5433 + + let entry = await store.entry(for: changed) + #expect(entry == nil) + } + + @Test func migratesLegacyInlineCacheWhenStoreEntryIsMissing() async throws { + let store = ObjectBrowserCacheStore(configuration: .init(rootDirectory: try makeTempDirectory())) + let structure = TestFixtures.databaseStructure(databaseCount: 1, schemasPerDatabase: 1, tablesPerSchema: 2) + let connection = SavedConnection( + id: UUID(), + connectionName: "Legacy", + host: "legacy.local", + port: 5432, + database: "legacydb", + username: "echo", + databaseType: .postgresql, + cachedStructure: structure, + cachedStructureUpdatedAt: Date(timeIntervalSince1970: 1_000) + ) + + await store.migrateLegacyCacheIfNeeded(from: connection, limitBytes: 512 * 1_024 * 1_024) + + let entry = await store.entry(for: connection) + #expect(entry?.structure == structure) + #expect(entry?.updatedAt == connection.cachedStructureUpdatedAt) + } + + @Test func prunesOldestEntriesFirstWhenOverLimit() async throws { + let directory = try makeTempDirectory() + let store = ObjectBrowserCacheStore(configuration: .init(rootDirectory: directory)) + let oldConnection = SavedConnection( + id: UUID(), + connectionName: "Old", + host: "old.local", + port: 5432, + database: "old", + username: "echo" + ) + let newConnection = SavedConnection( + id: UUID(), + connectionName: "New", + host: "new.local", + port: 5432, + database: "new", + username: "echo" + ) + + let oldEntry = ObjectBrowserCacheEntry( + key: ObjectBrowserCacheKey(connectionID: oldConnection.id), + connectionFingerprint: oldConnection.objectBrowserCacheFingerprint, + updatedAt: Date(timeIntervalSince1970: 1_000), + structure: TestFixtures.databaseStructure(databaseCount: 3, schemasPerDatabase: 2, tablesPerSchema: 10) + ) + let newEntry = ObjectBrowserCacheEntry( + key: ObjectBrowserCacheKey(connectionID: newConnection.id), + connectionFingerprint: newConnection.objectBrowserCacheFingerprint, + updatedAt: Date(timeIntervalSince1970: 2_000), + structure: TestFixtures.databaseStructure(databaseCount: 3, schemasPerDatabase: 2, tablesPerSchema: 10) + ) + + let encoder = JSONEncoder() + let oldData = try encoder.encode(oldEntry) + let newData = try encoder.encode(newEntry) + try oldData.write(to: directory.appendingPathComponent("\(oldConnection.id.uuidString).json")) + try newData.write(to: directory.appendingPathComponent("\(newConnection.id.uuidString).json")) + + await store.pruneToLimit(oldData.count + 1) + + #expect(await store.entry(for: oldConnection) == nil) + #expect(await store.entry(for: newConnection) != nil) + } + + private func makeTempDirectory() throws -> URL { + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("ObjectBrowserCacheStoreTests-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + return directory + } +} diff --git a/EchoTests/Services/QueryStatementClassifierTests.swift b/EchoTests/Services/QueryStatementClassifierTests.swift new file mode 100644 index 000000000..680445891 --- /dev/null +++ b/EchoTests/Services/QueryStatementClassifierTests.swift @@ -0,0 +1,34 @@ +import Testing +@testable import Echo + +struct QueryStatementClassifierTests { + @Test + func alterTableIsMessageOnly() { + #expect( + QueryStatementClassifier.isLikelyMessageOnlyStatement( + "ALTER TABLE dbo.people ADD nickname text NULL;", + databaseType: .microsoftSQL + ) + ) + } + + @Test + func selectRemainsResultSetStatement() { + #expect( + !QueryStatementClassifier.isLikelyMessageOnlyStatement( + "SELECT * FROM dbo.people;", + databaseType: .microsoftSQL + ) + ) + } + + @Test + func returningOverridesDmlClassification() { + #expect( + !QueryStatementClassifier.isLikelyMessageOnlyStatement( + "INSERT INTO people(name) VALUES ('Ana') RETURNING id;", + databaseType: .postgresql + ) + ) + } +} diff --git a/EchoTests/Services/SyncStartupDecisionTests.swift b/EchoTests/Services/SyncStartupDecisionTests.swift new file mode 100644 index 000000000..341b5e0f4 --- /dev/null +++ b/EchoTests/Services/SyncStartupDecisionTests.swift @@ -0,0 +1,54 @@ +import Testing +@testable import Echo + +@Suite("SyncStartupDecision") +struct SyncStartupDecisionTests { + + @Test func checkpointSuppressesStartupAction() { + let summary = SyncDataSummary( + localConnections: 2, + localIdentities: 1, + localFolders: 0, + localBookmarks: 0, + cloudDocuments: 4 + ) + + #expect(summary.startupAction(hasCheckpoint: true) == .none) + } + + @Test func bothSidesWithNoCheckpointRequiresMergePrompt() { + let summary = SyncDataSummary( + localConnections: 1, + localIdentities: 0, + localFolders: 0, + localBookmarks: 0, + cloudDocuments: 3 + ) + + #expect(summary.startupAction(hasCheckpoint: false) == .promptForMerge) + } + + @Test func cloudOnlyWithNoCheckpointPullsCloud() { + let summary = SyncDataSummary( + localConnections: 0, + localIdentities: 0, + localFolders: 0, + localBookmarks: 0, + cloudDocuments: 2 + ) + + #expect(summary.startupAction(hasCheckpoint: false) == .pullCloud) + } + + @Test func localOnlyWithNoCheckpointUploadsLocal() { + let summary = SyncDataSummary( + localConnections: 0, + localIdentities: 1, + localFolders: 1, + localBookmarks: 0, + cloudDocuments: 0 + ) + + #expect(summary.startupAction(hasCheckpoint: false) == .uploadLocal) + } +} diff --git a/EchoTests/Stores/ConnectionSessionTests.swift b/EchoTests/Stores/ConnectionSessionTests.swift index 49c863f9b..f42910bdc 100644 --- a/EchoTests/Stores/ConnectionSessionTests.swift +++ b/EchoTests/Stores/ConnectionSessionTests.swift @@ -187,4 +187,38 @@ final class ConnectionSessionTests: XCTestCase { let tab = cs.addQueryTab(withQuery: "SELECT 1") XCTAssertNotNil(tab) } + + func testHydrateMetadataFreshnessFromCacheStructureMarksCachedAndListOnly() async { + let cs = makeConnectionSession() + cs.databaseStructure = DatabaseStructure( + serverVersion: "16.0", + databases: [ + DatabaseInfo( + name: "loaded", + schemas: [SchemaInfo(name: "public", objects: [TestFixtures.schemaObjectInfo(name: "users")])] + ), + DatabaseInfo(name: "list_only", schemas: []) + ] + ) + + cs.hydrateMetadataFreshnessFromCacheStructure() + + XCTAssertEqual(cs.metadataFreshness(forDatabase: "loaded"), .cached) + XCTAssertEqual(cs.metadataFreshness(forDatabase: "list_only"), .listOnly) + } + + func testClearMetadataCacheStateResetsStructureAndFreshness() async { + let cs = makeConnectionSession() + cs.databaseStructure = TestFixtures.databaseStructure() + cs.hydrateMetadataFreshnessFromCacheStructure() + cs.markMetadataRefreshStarted(forDatabase: "db_0") + _ = cs.beginSchemaLoad(forDatabase: "db_0") + + cs.clearMetadataCacheState() + + XCTAssertNil(cs.databaseStructure) + XCTAssertEqual(cs.metadataFreshness(forDatabase: "db_0"), .listOnly) + XCTAssertTrue(cs.schemaLoadsInFlight.isEmpty) + XCTAssertEqual(cs.structureLoadingState, .idle) + } } diff --git a/README.md b/README.md index e96b38c54..a6b189af5 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,105 @@ -# Echo + -
+--- -**Echo** is a fast, lightweight, and truly native macOS database management client. Built from the ground up with SwiftUI and AppKit, it provides a seamless, Mac-first experience without the overhead of Electron or Java-based wrappers. +**Echo** is a high-performance, strictly native database management suite built exclusively for the modern macOS era. Eschewing the resource-heavy paradigms of Electron and Java, Echo leverages **Swift 6.2** and the **macOS 26 Tahoe** design language to deliver a tool that feels like a first-class citizen on your Mac. -Connect to your favorite relational databases, execute complex queries, and manage your data with speed and elegance. +Designed for engineers who demand precision, Echo combines a minimalist aesthetic with deep, dialect-specific functionality for PostgreSQL, Microsoft SQL Server, and more. --- -## ✨ Features +## ✨ Key Features -- **Truly Native:** Built with Swift and optimized for Apple Silicon and macOS 15+. Fast, responsive, and integrates deeply with native macOS features. -- **Multi-Database Support:** - - 🐘 PostgreSQL - - 🐬 MySQL - - 🪶 SQLite - - 🏢 Microsoft SQL Server (MSSQL) -- **Advanced SQL Editor:** Syntax highlighting, smart autocomplete, and a responsive query editing experience. -- **High-Performance Results Table:** Stream and navigate massive datasets instantly with native, hardware-accelerated grid rendering. -- **Intuitive Object Browser:** Seamlessly explore schemas, tables, views, and routines. -- **Security & Role Management:** Built-in tools for managing database logins, roles, parameters, and security labels. -- **Dark Mode Support:** Beautifully adapts to your system theme. +### 🧠 EchoSense™ Intelligence +Stop fighting with syntax. EchoSense is our context-aware SQL autocomplete engine that understands your schema, foreign keys, and dialect-specific quirks. It doesn't just suggest keywords; it predicts your intent. +- **Dialect-Aware:** Precise suggestions for T-SQL, PL/pgSQL, and SQLite. +- **Schema Navigation:** Autocomplete tables, columns, and joins based on live metadata. ---- +### ⚙️ Activity Engine +Long-running operations shouldn't be a black box. Our centralized **Activity Engine** tracks backups, restores, and maintenance tasks directly in the native toolbar. +- **Real-time Progress:** Visual feedback for every background task. +- **Detailed History:** Audit exactly what happened and when. -## 🚀 Installation +### 🛠️ Professional Maintenance Suite +Go beyond simple queries. Echo provides deep integration for database administration: +- **MSSQL Power Tools:** Rebuild indexes, check integrity, and manage agent jobs with native UI. +- **Postgres Management:** Vacuum, reindex, and security label management. +- **Schema Browser:** A lightning-fast, hierarchical view of your entire database structure. -### Via Homebrew (Recommended) -The easiest way to install and keep Echo updated is via Homebrew. Using the `--no-quarantine` flag is recommended to bypass macOS Gatekeeper (as the open-source releases are currently ad-hoc signed): +### 📊 Streaming Query Workspace +Experience zero-lag result sets. Echo streams data directly from the wire to a hardware-accelerated grid. +- **Execution Plans:** Visualize how your queries run to find bottlenecks. +- **Native Rendering:** Handles millions of rows with minimal memory footprint. -```bash -brew install --cask --no-quarantine tashda/tap/echo -``` +--- + +## 🏗️ The Engine: Custom-Built Native Drivers + +Unlike other database clients that rely on generic, multi-platform libraries, Echo is powered by a suite of **first-party, custom-built database drivers**. We maintain the entire stack to ensure absolute performance, memory efficiency, and seamless integration with Swift's modern concurrency model. -### Manual Download -You can download the latest compiled `.zip` from the [Releases](https://github.com/tashda/Echo/releases) page. +- **[postgres-wire](https://github.com/tashda/postgres-wire):** A high-performance, pure Swift implementation of the PostgreSQL wire protocol. No C-dependencies, just raw speed. +- **[sqlserver-nio](https://github.com/tashda/sqlserver-nio):** Built on SwiftNIO, this driver provides an asynchronous, non-blocking bridge to Microsoft SQL Server, supporting advanced T-SQL features. +- **[mysql-wire](https://github.com/tashda/mysql-wire):** Our native MySQL implementation, currently in active development to bring the same performance standards to the MySQL ecosystem. -*Note: If you install manually without Homebrew, you may need to **Right-Click** the app and select **Open** the first time you launch it to bypass the macOS "unverified developer" warning.* +### What this means for you: +- **Zero Overhead:** No translation layers between the UI and the database socket. +- **Data-Race Safety:** Built from the ground up for **Swift 6.2**, ensuring compile-time safety for your data. +- **Native Efficiency:** Minimal memory footprint even when streaming millions of rows. --- -## 🔄 Auto-Updates -Echo uses the [Sparkle](https://sparkle-project.org) framework for secure automatic updates via EdDSA (Ed25519) cryptographic signatures. +## 🗄️ Supported Databases -You can manually check for updates at any time via the menu bar: **Echo > Check for Updates...** or simply wait for the app to notify you when a new version is released. +| Database | Status | Features | +| :--- | :--- | :--- | +| **PostgreSQL** | 🟢 Stable | Streaming, Metadata, Security, Maintenance | +| **Microsoft SQL Server** | 🟢 Stable | T-SQL, Agent Jobs, Maintenance, Indexing | +| **SQLite** | 🟢 Stable | Local browser, Full Schema Support | +| **MySQL** | 🟡 Beta | Query Execution, Table Exploration | --- -## 🛠️ Developer Setup & CI/CD +## 🚀 Installation -Interested in building Echo from source or contributing? Echo uses the Swift Package Manager (SPM) and is built strictly for modern macOS architectures. +Echo is distributed as a standalone, signed macOS application. -1. Clone the repository. -2. Open `Echo.xcodeproj` in **Xcode 16+**. -3. Let SPM resolve the required dependencies. -4. Select the `Echo` scheme and hit `Cmd + R` to build and run. +1. **Download:** Grab the latest release from the [GitHub Releases](https://github.com/tashda/Echo/releases) page or visit [echodb.dev](https://echodb.dev). +2. **Move to Applications:** Drag `Echo.app` into your `/Applications` folder. +3. **Auto-Updates:** Echo includes a built-in update mechanism powered by **Sparkle**. You will be notified automatically when a new version is available. -### Automated Releases -This repository is configured with a fully automated GitHub Actions pipeline (`.github/workflows/build-release.yml`). +--- -- **Trigger:** Any push or pull-request merge to the `main` branch. -- **Process:** The workflow builds the app (Release configuration), packages it into a ZIP, signs the update with Sparkle keys, and publishes a new GitHub Release. -- **Appcast:** The Sparkle update feed (`appcast.xml`) is automatically generated and hosted alongside each release. +## 🛠️ Developer Setup -*(Note for maintainers: To run the pipeline, ensure `SPARKLE_PRIVATE_KEY` is set in the repository's Actions Secrets).* +```bash +# Clone the repository +git clone https://github.com/tashda/Echo.git ---- +# Open in Xcode 26+ +open Echo.xcodeproj -## 📝 License +# Build and Run +# Ensure the 'Echo' scheme is selected (Cmd + R) +``` + +--- -*(License information to be added)* +
+

Built with ❤️ by the Echo Team.

+

echodb.dev

+
diff --git a/proxmox_mcp.log b/proxmox_mcp.log index 4c218637b..82cbadd02 100644 --- a/proxmox_mcp.log +++ b/proxmox_mcp.log @@ -142,3 +142,9 @@ 2026-04-02 20:59:30,485 - mcp.server.lowlevel.server - INFO - Processing request of type ListToolsRequest 2026-04-02 20:59:30,485 - mcp.server.lowlevel.server - INFO - Processing request of type ListPromptsRequest 2026-04-02 20:59:30,486 - mcp.server.lowlevel.server - INFO - Processing request of type ListResourcesRequest +2026-04-04 23:05:48,386 - proxmox-mcp.proxmox - INFO - Connecting to Proxmox host: 192.168.1.150 +2026-04-04 23:05:48,633 - proxmox-mcp.proxmox - INFO - Successfully connected to Proxmox API +2026-04-04 23:05:48,641 - proxmox-mcp - INFO - Starting MCP server... +2026-04-04 23:05:48,649 - mcp.server.lowlevel.server - INFO - Processing request of type ListToolsRequest +2026-04-04 23:05:48,650 - mcp.server.lowlevel.server - INFO - Processing request of type ListPromptsRequest +2026-04-04 23:05:48,650 - mcp.server.lowlevel.server - INFO - Processing request of type ListResourcesRequest From 7a1d7b1ef749f3c7e5ae64db64d104be72704541 Mon Sep 17 00:00:00 2001 From: tashda Date: Thu, 9 Apr 2026 10:31:53 +0200 Subject: [PATCH 2/3] Fix ObjectBrowserCacheStore pruning logic for small limits The hardcoded 64MB minimum in 'pruneToLimit' was preventing the unit tests from verifying the LRU pruning behavior with small test data. Removed the minimum to allow granular control over cache limits. --- .../Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift b/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift index 2161da096..479dedbf2 100644 --- a/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift +++ b/Echo/Sources/Features/ObjectBrowser/Cache/ObjectBrowserCacheStore.swift @@ -90,7 +90,7 @@ actor ObjectBrowserCacheStore { } func pruneToLimit(_ limitBytes: Int) async { - let normalizedLimit = max(limitBytes, 64 * 1_024 * 1_024) + let normalizedLimit = limitBytes var entries: [(url: URL, updatedAt: Date, size: Int)] = cacheFileURLs().compactMap { url in guard let data = try? Data(contentsOf: url), let entry = try? decoder.decode(ObjectBrowserCacheEntry.self, from: data) else { From b0239fef6ce0dfd9baea3a43539b2bc1809c1d06 Mon Sep 17 00:00:00 2001 From: tashda Date: Thu, 9 Apr 2026 11:22:33 +0200 Subject: [PATCH 3/3] Fix crash in MSSQLDataTypePicker unit tests Marked test methods as @MainActor to ensure they run on the same actor as the picker's static utility methods. This prevents data races and crashes observed in the CI environment when accessing actor-isolated static properties from background threads. --- EchoTests/Components/MSSQLDataTypePickerTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EchoTests/Components/MSSQLDataTypePickerTests.swift b/EchoTests/Components/MSSQLDataTypePickerTests.swift index 60621eb4c..5c9a2c1d6 100644 --- a/EchoTests/Components/MSSQLDataTypePickerTests.swift +++ b/EchoTests/Components/MSSQLDataTypePickerTests.swift @@ -4,7 +4,7 @@ import Testing @Suite("MSSQLDataTypePicker") struct MSSQLDataTypePickerTests { - @Test func preservesBareUnicodeTypeWithoutInjectingDefaultLength() { + @Test @MainActor func preservesBareUnicodeTypeWithoutInjectingDefaultLength() { let state = MSSQLDataTypePicker.selectionState(for: "nvarchar") #expect(state.baseType == "nvarchar") @@ -12,7 +12,7 @@ struct MSSQLDataTypePickerTests { #expect(state.isCustom == false) } - @Test func preservesExplicitLengthForParameterizedType() { + @Test @MainActor func preservesExplicitLengthForParameterizedType() { let state = MSSQLDataTypePicker.selectionState(for: "nvarchar(4000)") #expect(state.baseType == "nvarchar")