From 4663cebd845b3341978c1327217840f6fc248467 Mon Sep 17 00:00:00 2001 From: cricketthomas Date: Tue, 24 Mar 2026 15:07:23 -0400 Subject: [PATCH 1/5] fix null issue with db context saving add documentation to login page with locaizaiton fix localization save message add more error catching add place holder for loading (gpt helped) REMOVE search service, its utterly broken add back filter service --- .../AzureKeyVaultStudio/App.xaml.cs | 1 - .../AzureKeyVaultStudio/Database/DbContext.cs | 8 + .../Models/FilterService.cs | 7 + .../Models/KeyVaultModel.cs | 30 ++- .../Models/KeyVaultResourcePlaceholder.cs | 19 +- .../Presentation/LoginPage.xaml | 7 + .../Presentation/MainViewModel.cs | 2 +- .../Presentation/SettingsViewModel.cs | 19 +- .../Properties/launchSettings.json | 18 +- .../Services/AzureSearchService.cs | 174 ------------------ .../Strings/en/Resources.resw | 3 + .../Strings/es/Resources.resw | 3 + .../Strings/fr/Resources.resw | 3 + .../Strings/pt-BR/Resources.resw | 3 + .../UserControls/KeyVaultTree.xaml | 1 + .../UserControls/KeyVaultTree.xaml.cs | 7 +- .../UserControls/OverrideTitlebar.xaml | 6 +- .../ViewModels/KeyVaultTreeViewModel.cs | 131 +++++++++---- 18 files changed, 193 insertions(+), 249 deletions(-) delete mode 100644 src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Services/AzureSearchService.cs diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs index 876bc9c9..e22ede72 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs @@ -152,7 +152,6 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) services.AddSingleton(_ => LocalSettingsServiceFactory.Create()); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddTransient(); }).UseNavigation(RegisterRoutes)); diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Database/DbContext.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Database/DbContext.cs index 42b9908d..49c528e5 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Database/DbContext.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Database/DbContext.cs @@ -18,6 +18,9 @@ public void Dispose() public static async Task DeleteQuickAccessItemByKeyVaultId(string keyVaultId) { + if (keyVaultId is null) + return true; + using var connection = await TryCreateDatabaseAndOpenConnection(); await connection.OpenAsync(); using var command = connection.CreateCommand(); @@ -94,6 +97,8 @@ public static async Task> GetStoredSubscriptions(string tena public static async Task InsertQuickAccessItemAsync(QuickAccess item) { + if (item is null) + return; using var connection = await TryCreateDatabaseAndOpenConnection(); await connection.OpenAsync(); using var command = connection.CreateCommand(); @@ -131,6 +136,9 @@ public static async Task InsertSubscriptions(IEnumerable subscrip public static async Task QuickAccessItemByKeyVaultIdExists(string? keyVaultId) { + if (keyVaultId is null) + return true; + using var connection = await TryCreateDatabaseAndOpenConnection(); await connection.OpenAsync(); using var command = connection.CreateCommand(); diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/FilterService.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/FilterService.cs index f0c155d4..a98d58a6 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/FilterService.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/FilterService.cs @@ -39,6 +39,13 @@ static void SetResourceGroupExpanded(KvResourceGroupModel model, bool value) foreach (var subscription in allSubscriptions) { + if (subscription.Type == KvSubscriptionModel.ExplorerItemType.QuickAccess) + { + SetSubscriptionExpanded(subscription, true); + results.Add(subscription); + continue; + } + bool isMatch = false; if (ContainsQuery(subscription.DisplayName, querySpan)) diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultModel.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultModel.cs index 19e566f4..52d77d76 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultModel.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultModel.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Diagnostics; using Azure.ResourceManager.KeyVault; using Azure.ResourceManager.Resources; @@ -67,19 +68,28 @@ internal partial class ExplorerItemTemplateSelector : DataTemplateSelector protected override DataTemplate SelectTemplateCore(object item) { - if (item is KvSubscriptionModel model) + try { - if (model.Type == KvSubscriptionModel.ExplorerItemType.QuickAccess) - return PinnedItemTemplate; - else return SubscriptionTemplate; - } + if (item is KvSubscriptionModel model) + { + if (model.Type == KvSubscriptionModel.ExplorerItemType.QuickAccess) + return PinnedItemTemplate; + else return SubscriptionTemplate; + } - if (item is KvResourceGroupModel) - return ResourceGroupTemplate; + if (item is KvResourceGroupModel) + return ResourceGroupTemplate; - if (item is KeyVaultResource) - return KeyVaultResourceTemplate; + if (item is KeyVaultResource) + return KeyVaultResourceTemplate; - return base.SelectTemplateCore(item); + return base.SelectTemplateCore(item); + } + catch (Exception ex) + { + Debug.WriteLine($"Error selecting template for item: {item}"); + return base.SelectTemplateCore(item); + + } } } diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultResourcePlaceholder.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultResourcePlaceholder.cs index 77510110..2f612cfe 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultResourcePlaceholder.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultResourcePlaceholder.cs @@ -6,15 +6,20 @@ namespace AzureKeyVaultStudio.Models; public class KeyVaultResourcePlaceholder : KeyVaultResource { - public override ResourceIdentifier Id => base.Id; - public override KeyVaultData? Data + private static readonly ResourceIdentifier PlaceholderId = + new("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/loading/providers/Microsoft.KeyVault/vaults/loading"); + + private static KeyVaultData CreatePlaceholderData() { - get - { - KeyVaultData? keyVaultData = base.Data ?? null; - return keyVaultData; - } + var properties = new KeyVaultProperties(Guid.Empty, new KeyVaultSku(KeyVaultSkuFamily.A, KeyVaultSkuName.Standard)); + return new KeyVaultDataPlaceholder(AzureLocation.EastUS2, properties); } + + private readonly KeyVaultData _placeholderData = CreatePlaceholderData(); + + public override ResourceIdentifier Id => PlaceholderId; + + public override KeyVaultData? Data => _placeholderData; } public class KeyVaultDataPlaceholder : KeyVaultData diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/LoginPage.xaml b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/LoginPage.xaml index 3cd32c9b..1325bb2e 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/LoginPage.xaml +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/LoginPage.xaml @@ -38,6 +38,13 @@ x:Uid="LoginPageGettingStartedMessage" Opacity="0.8" TextWrapping="Wrap" /> + + diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/MainViewModel.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/MainViewModel.cs index bd9fcb34..a1320d85 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/MainViewModel.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/MainViewModel.cs @@ -143,7 +143,7 @@ public void ClosePane() } private const double PaneMinWidth = 100; - private const double PaneMaxWidth = 700; + private const double PaneMaxWidth = 1000; public double InvertedSplitViewWidth { diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/SettingsViewModel.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/SettingsViewModel.cs index 09227e9a..ab31c6b3 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/SettingsViewModel.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Presentation/SettingsViewModel.cs @@ -54,6 +54,7 @@ public partial class SettingsViewModel : ObservableRecipient [ObservableProperty] public partial AuthenticatedUserClaims? Claims { get; set; } + public string? GitHubRepoistoryBaseUrl => _appConfig.Value.GitHubRepoistoryBaseUrl; public string? LicenseUrl => _appConfig.Value.LicenseUrl; public string? NewIssueUrl => _appConfig.Value.NewIssueUrl; @@ -146,27 +147,25 @@ private void LoadSettings() private async Task SaveSettings() { var selectedCloud = SelectedCloudEnvironmentIndex >= 0 && SelectedCloudEnvironmentIndex < AzureCloudInstances.Length - ? AzureCloudInstances[SelectedCloudEnvironmentIndex] - : AzureCloudInstance.None; + ? AzureCloudInstances[SelectedCloudEnvironmentIndex] + : AzureCloudInstance.None; Save(Constants.SelectedCloudEnvironmentName, (int)selectedCloud); Save(nameof(SettingsPageClientIdCheckbox), SettingsPageClientIdCheckbox); Save(nameof(SettingsPageTenantIdCheckbox), SettingsPageTenantIdCheckbox); Save(nameof(ClearClipboardTimeout), ClearClipboardTimeout < 0 ? 10 : ClearClipboardTimeout); Save(nameof(CustomClientId), CustomClientId?.Trim() ?? string.Empty); Save(nameof(CustomTenantId), CustomTenantId?.Trim() ?? string.Empty); - await _localizationService.SetCurrentCultureAsync(Language); + _ = _localizationService.SetCurrentCultureAsync(Language); WeakReferenceMessenger.Default.Send(new SendInAppNotificationMessage(new Notification { Severity = InfoBarSeverity.Informational, - Title = _localizer["SavedTitle"] ?? "Saved.", - Message = _localizer["SavedChangesMessage"] ?? "Your changes have been saved.", + Title = _localizer?["SavedTitle"] ?? "Saved.", + Message = _localizer?["SavedChangesMessage"] ?? "Your changes have been saved.", Duration = TimeSpan.FromSeconds(5) })); - } - [RelayCommand] private async Task SignInOrRefreshTokenAsync() { @@ -200,7 +199,7 @@ private async Task DeleteDatabase() WeakReferenceMessenger.Default.Send(new SendInAppNotificationMessage(new Notification { Severity = InfoBarSeverity.Warning, - Message = _localizer["DatabseDeletedMessage"] ?? "Database deleted. Restart the app to recreate the database.", + Message = _localizer?["DatabseDeletedMessage"] ?? "Database deleted. Restart the app to recreate the database.", Title = "Info" })); } @@ -219,7 +218,7 @@ private async Task ResetApplicationState() WeakReferenceMessenger.Default.Send(new SendInAppNotificationMessage(new Notification { Severity = InfoBarSeverity.Warning, - Message = _localizer["ApplicationResetMessage"] ?? "Application has been reset. Please exit the app.", + Message = _localizer?["ApplicationResetMessage"] ?? "Application has been reset. Please exit the app.", Title = "Danger" })); } @@ -237,6 +236,4 @@ public static string GetAppVersion() var version = assembly.GetName().Version; return version == null ? "(Unknown)" : $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; } - - //NotificationQueue.Show(notification); } diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Properties/launchSettings.json b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Properties/launchSettings.json index 2c3d65b8..69cb4d9a 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Properties/launchSettings.json +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Properties/launchSettings.json @@ -1,12 +1,4 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:8080", - "sslPort": 0 - } - }, "profiles": { "AzureKeyVaultStudio (WinAppSDK Unpackaged)": { "commandName": "Project", @@ -26,5 +18,13 @@ "distributionName": "", "compatibleTargetFramework": "desktop" } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8080", + "sslPort": 0 + } } -} +} \ No newline at end of file diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Services/AzureSearchService.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Services/AzureSearchService.cs deleted file mode 100644 index b18f32c8..00000000 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Services/AzureSearchService.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System.Collections.ObjectModel; -using Azure.ResourceManager.KeyVault; -using Azure.ResourceManager.Resources; - -namespace AzureKeyVaultStudio.Services; - -public class AzureSearchService -{ - private readonly VaultService _vaultService; - - public AzureSearchService(VaultService vaultService) - { - _vaultService = vaultService; - } - - public async Task> SearchAsync(string query, KvSubscriptionModel? quickAccessNode = null, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(query)) - return []; - - var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = 3, CancellationToken = cancellationToken }; - var subscriptions = await _vaultService.GetSubscriptionsAsync(); - - static bool Contains(string? value, string q) - => !string.IsNullOrEmpty(value) && value.Contains(q, StringComparison.OrdinalIgnoreCase); - - var subMatches = subscriptions - .Where(s => Contains(s.Data?.DisplayName, query)) - .ToList(); - - if (subMatches.Count > 0) - { - var subResults = subMatches.Select(s => new KvSubscriptionModel - { - DisplayName = s.Data.DisplayName, - SubscriptionId = s.Data.Id, - Subscription = s, - IsExpanded = true, - ResourceGroups = [new KvResourceGroupModel - { - DisplayName = string.Empty, - KeyVaultResources = [new KeyVaultResourcePlaceholder()] - }] - }).ToList(); - - return PrependQuickAccess(subResults, quickAccessNode, Contains, query); - } - - cancellationToken.ThrowIfCancellationRequested(); - - var rgResults = await SearchByResourceGroupAsync(subscriptions, parallelOptions, Contains, query); - if (rgResults.Count > 0) - return PrependQuickAccess(rgResults, quickAccessNode, Contains, query); - - cancellationToken.ThrowIfCancellationRequested(); - - var kvResults = await SearchByKeyVaultAsync(subscriptions, parallelOptions, Contains, query); - return PrependQuickAccess(kvResults, quickAccessNode, Contains, query); - } - - private static List PrependQuickAccess( - List results, - KvSubscriptionModel? quickAccessNode, - Func contains, - string query) - { - if (quickAccessNode is null) - return results; - - var matchingPins = quickAccessNode.PinnedItems - .Where(kv => kv.HasData && contains(kv.Data.Name, query)) - .ToList(); - - var filteredNode = new KvSubscriptionModel - { - DisplayName = quickAccessNode.DisplayName, - Type = KvSubscriptionModel.ExplorerItemType.QuickAccess, - IsExpanded = true, - PinnedItems = new ObservableCollection(matchingPins) - }; - - return [filteredNode, .. results]; - } - - private static async Task> SearchByResourceGroupAsync(IEnumerable subscriptions, ParallelOptions parallelOptions, Func contains, string query) - { - var results = new List(); - - await Parallel.ForEachAsync(subscriptions, parallelOptions, async (sub, ct) => - { - ct.ThrowIfCancellationRequested(); - var matchingRgs = new List(); - - await foreach (var rg in sub.GetResourceGroups()) - { - ct.ThrowIfCancellationRequested(); - - if (!contains(rg.Data?.Name, query)) - continue; - - var rgModel = new KvResourceGroupModel - { - DisplayName = rg.Data.Name, - ResourceGroupResource = rg, - IsExpanded = true, - KeyVaultResources = [] - }; - - await foreach (var kv in rg.GetKeyVaults()) - { - ct.ThrowIfCancellationRequested(); - rgModel.KeyVaultResources.Add(kv); - } - - matchingRgs.Add(rgModel); - } - - if (matchingRgs.Count > 0) - results.Add(ToSubscriptionModel(sub, matchingRgs)); - }); - - return results; - } - - private static async Task> SearchByKeyVaultAsync(IEnumerable subscriptions, ParallelOptions parallelOptions, Func contains, string query) - { - var results = new List(); - - await Parallel.ForEachAsync(subscriptions, parallelOptions, async (sub, ct) => - { - ct.ThrowIfCancellationRequested(); - var matchingRgs = new List(); - - await foreach (var rg in sub.GetResourceGroups()) - { - ct.ThrowIfCancellationRequested(); - var matchingKvs = new List(); - - await foreach (var kv in rg.GetKeyVaults()) - { - ct.ThrowIfCancellationRequested(); - if (kv.HasData && contains(kv.Data.Name, query)) - matchingKvs.Add(kv); - } - - if (matchingKvs.Count > 0) - { - matchingRgs.Add(new KvResourceGroupModel - { - DisplayName = rg.Data.Name, - ResourceGroupResource = rg, - IsExpanded = true, - KeyVaultResources = new ObservableCollection(matchingKvs) - }); - } - } - - if (matchingRgs.Count > 0) - results.Add(ToSubscriptionModel(sub, matchingRgs)); - }); - - return results; - } - - private static KvSubscriptionModel ToSubscriptionModel(SubscriptionResource sub, List rgs) => - new() - { - DisplayName = sub.Data.DisplayName, - SubscriptionId = sub.Data.Id, - Subscription = sub, - IsExpanded = true, - ResourceGroups = new ObservableCollection(rgs) - }; -} diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/en/Resources.resw b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/en/Resources.resw index df48fb7c..5f31bba6 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/en/Resources.resw +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/en/Resources.resw @@ -666,4 +666,7 @@ Your changes have been saved. + + Documentation: Tenant setup guide + \ No newline at end of file diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/es/Resources.resw b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/es/Resources.resw index 8d552030..ec874485 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/es/Resources.resw +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/es/Resources.resw @@ -666,4 +666,7 @@ Sus cambios han sido guardados. + + Documentación: Guía de configuración del tenant + \ No newline at end of file diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/fr/Resources.resw b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/fr/Resources.resw index 22326721..1102d977 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/fr/Resources.resw +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/fr/Resources.resw @@ -666,4 +666,7 @@ Vos modifications ont été enregistrées. + + Documentation : Guide de configuration du tenant + \ No newline at end of file diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/pt-BR/Resources.resw b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/pt-BR/Resources.resw index d913a23d..3370399d 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/pt-BR/Resources.resw +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Strings/pt-BR/Resources.resw @@ -666,4 +666,7 @@ Suas alterações foram salvas. + + Documentação: Guia de configuração do tenant + \ No newline at end of file diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml index 0fb6e01f..0454d15b 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml @@ -246,6 +246,7 @@ diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs index 364ca509..bdc2b40d 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs @@ -39,7 +39,7 @@ private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEve if (ViewModel?.RefreshCommand is not null) { Bindings.Update(); - ViewModel.RefreshCommand.Execute(CancellationToken.None); + ViewModel.RefreshCommand.Execute(null); } } @@ -91,4 +91,9 @@ private void SubNodeCollectionChanged(object? sender, NotifyCollectionChangedEve private void ResourceGroupNodePropertyChanged(object? sender, PropertyChangedEventArgs e) { } + private void KeyVaultTreeView_DragItemsStarting(TreeView sender, TreeViewDragItemsStartingEventArgs args) + { + args.Cancel = true; + return; + } } diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/OverrideTitlebar.xaml b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/OverrideTitlebar.xaml index efebc3b2..66494c22 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/OverrideTitlebar.xaml +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/OverrideTitlebar.xaml @@ -19,7 +19,7 @@ _subscriptionsLoadingSubNodes = new(); - public KeyVaultTreeViewModel(AuthService authService, VaultService vaultService, IDispatcher dispatcher, AzureSearchService searchService, IStringLocalizer localizer) + public KeyVaultTreeViewModel(AuthService authService, VaultService vaultService, IDispatcher dispatcher, IStringLocalizer localizer) { _authService = authService; _vaultService = vaultService; _dispatcher = dispatcher; - _searchService = searchService; _localizer = localizer; TreeDataSource.CollectionChanged += TreeViewList_CollectionChanged; @@ -100,7 +98,8 @@ private async Task Refresh(CancellationToken token) #if DEBUG //await Task.Delay(4000, token); #endif - _ = Task.Run(() => InitializeTreeDataSource(token), token); + await ClearAndResetTreeAsync(); + await InitializeTreeDataSource(token); } [RelayCommand] @@ -124,9 +123,9 @@ private async Task RemovePinVaultToQuickAccess(KeyVaultResource model) private async Task InitializeTreeDataSource(CancellationToken token) { - var items = await Task.Run(async () => + IsBusy = true; + try { - IsBusy = true; var subscriptionModel = new ObservableCollection(); var resource = _vaultService.GetKeyVaultResourceBySubscription(); @@ -154,8 +153,8 @@ private async Task InitializeTreeDataSource(CancellationToken token) var savedItems = DbContext.GetQuickAccessItemsAsyncEnumerable(_authService.TenantId ?? null); var tokenString = await _authService.GetAzureArmTokenSilent(); - var token = new CustomTokenCredential(tokenString); - var armClient = new ArmClient(token); + var tokenCredential = new CustomTokenCredential(tokenString); + var armClient = new ArmClient(tokenCredential); await foreach (var item in savedItems) { @@ -175,12 +174,6 @@ private async Task InitializeTreeDataSource(CancellationToken token) } } - // assign to subscriptionModel[0] if there are items in the collection, since wee need to show pinned at al times. - if (subscriptionModel.Count > 0) - { - subscriptionModel[0].PinnedItems = new ObservableCollection(quickAccess.PinnedItems); - } - subscriptionModel.Insert(0, quickAccess); foreach (var sub in subscriptionModel) @@ -196,19 +189,22 @@ private async Task InitializeTreeDataSource(CancellationToken token) Debug.WriteLine($"Error in InitializeTreeDataSource: {ex}"); } - return subscriptionModel; - }, cancellationToken: token); + if (_dispatcher != null) + { + _dispatcher.TryEnqueue(() => + { + SelectedItem = null; + TreeDataSource = []; + TreeDataSource = new ObservableCollection(subscriptionModel); + }); + } - if (_dispatcher != null) + TreeDataSourceReadOnly = [.. subscriptionModel]; + } + finally { - _dispatcher.TryEnqueue(() => - { - TreeDataSource = new ObservableCollection(items); - }); + IsBusy = false; } - - TreeDataSourceReadOnly = items.ToImmutableList(); - IsBusy = false; } private void KvPinnedModel_PropertyChanged(object sender, PropertyChangedEventArgs e) @@ -315,7 +311,7 @@ await Task.Run(async () => _dispatcher.TryEnqueue(() => { - foreach (var rgModel in rgList) + foreach (var rgModel in rgList.OrderBy(x => x.DisplayName)) { kvSubModel.ResourceGroups.Add(rgModel); } @@ -339,21 +335,33 @@ await Task.Run(async () => [RelayCommand(IncludeCancelCommand = true, AllowConcurrentExecutions = false)] - private async Task ExecuteSearch(CancellationToken token) + private Task ExecuteSearch(CancellationToken token) { + if (token.IsCancellationRequested) + return Task.CompletedTask; + if (string.IsNullOrWhiteSpace(SearchQuery)) { - _dispatcher.TryEnqueue(() => TreeDataSource = new ObservableCollection(TreeDataSourceReadOnly)); - return; + _dispatcher.TryEnqueue(() => + { + SelectedItem = null; + TreeDataSource = new ObservableCollection(TreeDataSourceReadOnly); + }); + return Task.CompletedTask; } - var quickAccessNode = TreeDataSource.FirstOrDefault(s => s.Type == KvSubscriptionModel.ExplorerItemType.QuickAccess); - var results = await _searchService.SearchAsync(SearchQuery, quickAccessNode, token); + + var source = TreeDataSourceReadOnly.Count > 0 + ? TreeDataSourceReadOnly + : [.. TreeDataSource]; + + var results = FilterService.Filter(source, SearchQuery); _dispatcher.TryEnqueue(() => { + SelectedItem = null; TreeDataSource = new ObservableCollection(results); }); - + return Task.CompletedTask; } [RelayCommand] @@ -413,4 +421,65 @@ internal void SetDispatcher(IDispatcher dispatcher) { _dispatcher = dispatcher; } + + private Task ClearAndResetTreeAsync() + { + if (_dispatcher is null) + return Task.CompletedTask; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _dispatcher.TryEnqueue(() => + { + SelectedItem = null; + TreeDataSource = []; + TreeDataSourceReadOnly = []; + tcs.TrySetResult(); + }); + + return tcs.Task; + } + + partial void OnTreeDataSourceChanging(ObservableCollection? oldValue, ObservableCollection? newValue) + { + if (oldValue is null) + return; + + oldValue.CollectionChanged -= TreeViewList_CollectionChanged; + + foreach (var sub in oldValue) + { + sub.PropertyChanged -= KvSubscriptionModel_PropertyChanged; + sub.ResourceGroups.CollectionChanged -= TreeViewSubNode_CollectionChanged; + + foreach (var rg in sub.ResourceGroups) + { + rg.PropertyChanged -= KvResourceGroupNode_PropertyChanged; + } + } + } + + partial void OnTreeDataSourceChanged(ObservableCollection? value) + { + if (value is null) + return; + + value.CollectionChanged -= TreeViewList_CollectionChanged; + value.CollectionChanged += TreeViewList_CollectionChanged; + + foreach (var sub in value) + { + sub.PropertyChanged -= KvSubscriptionModel_PropertyChanged; + sub.PropertyChanged += KvSubscriptionModel_PropertyChanged; + + sub.ResourceGroups.CollectionChanged -= TreeViewSubNode_CollectionChanged; + sub.ResourceGroups.CollectionChanged += TreeViewSubNode_CollectionChanged; + + foreach (var rg in sub.ResourceGroups) + { + rg.PropertyChanged -= KvResourceGroupNode_PropertyChanged; + rg.PropertyChanged += KvResourceGroupNode_PropertyChanged; + } + } + } } From cc8e0749701fc567208b104f60618aa36535d88d Mon Sep 17 00:00:00 2001 From: cricketthomas Date: Tue, 24 Mar 2026 16:34:16 -0400 Subject: [PATCH 2/5] add more error handling --- .../AzureKeyVaultStudio/App.xaml.cs | 8 ++ .../ViewModels/KeyVaultTreeViewModel.cs | 83 ++++++++++++------- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs index e22ede72..ac5d615f 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs @@ -24,6 +24,14 @@ public App() Debug.WriteLine(e.Exception.StackTrace); }; + TaskScheduler.UnobservedTaskException += (sender, e) => + { + Console.WriteLine("Unobserved exception caught!"); + foreach (var ex in e.Exception.Flatten().InnerExceptions) + Console.WriteLine(ex); + e.SetObserved(); + }; + UnhandledException += (sender, args) => { Debug.WriteLine($"[Unhandled] {args.Exception}"); diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs index b024fd5e..fa7fe001 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs @@ -66,9 +66,11 @@ public KeyVaultTreeViewModel(AuthService authService, VaultService vaultService, [RelayCommand] public void ExpandAll() => _ = Task.Run(() => _dispatcher.TryEnqueue(() => TreeDataSource.ForEach(item => item.IsExpanded = true))); - [RelayCommand] + [RelayCommand(FlowExceptionsToTaskScheduler = true)] public async Task PinVaultToQuickAccess(KeyVaultResource model) { + if (model is null) + return; var exists = await DbContext.QuickAccessItemByKeyVaultIdExists(model.Id); if (exists) return; var qa = new QuickAccess @@ -98,13 +100,16 @@ private async Task Refresh(CancellationToken token) #if DEBUG //await Task.Delay(4000, token); #endif - await ClearAndResetTreeAsync(); + await ClearAndResetTree(); await InitializeTreeDataSource(token); } [RelayCommand] private async Task RemovePinVaultToQuickAccess(KeyVaultResource model) { + if (model is null) + return; + var exists = await DbContext.QuickAccessItemByKeyVaultIdExists(model.Id); if (!exists) return; @@ -232,31 +237,7 @@ private async void KvResourceGroupNode_PropertyChanged(object? sender, PropertyC if (e.PropertyName == nameof(KvResourceGroupModel.IsSelected)) kvResourceModel.IsExpanded = true; - var hasPlaceholder = kvResourceModel.KeyVaultResources.Any(k => k.GetType().Name == nameof(KeyVaultResourcePlaceholder)); - // if its being expanded and there are no items in the array reach out to azure - if (kvResourceModel.IsExpanded && hasPlaceholder) - { - kvResourceModel.KeyVaultResources.Clear(); - - await Task.Run(async () => - { - var vaults = _vaultService.GetKeyVaultsByResourceGroup(kvResourceModel.ResourceGroupResource); - var vaultsList = new List(); - - await foreach (var vault in vaults) - { - vaultsList.Add(vault); - } - - _dispatcher.TryEnqueue(() => - { - foreach (var vault in vaultsList) - { - kvResourceModel.KeyVaultResources.Add(vault); - } - }); - }); - } + await LoadResourceGroupVaults(kvResourceModel); } } @@ -332,8 +313,6 @@ await Task.Run(async () => } } - - [RelayCommand(IncludeCancelCommand = true, AllowConcurrentExecutions = false)] private Task ExecuteSearch(CancellationToken token) { @@ -406,6 +385,9 @@ private void TreeViewSubNode_CollectionChanged(object? sender, NotifyCollectionC foreach (KvResourceGroupModel newItem in e.NewItems) { newItem.PropertyChanged += KvResourceGroupNode_PropertyChanged; + + if (newItem.IsExpanded) + _ = LoadResourceGroupVaults(newItem); } } else if (e.Action == NotifyCollectionChangedAction.Remove) @@ -422,7 +404,7 @@ internal void SetDispatcher(IDispatcher dispatcher) _dispatcher = dispatcher; } - private Task ClearAndResetTreeAsync() + private Task ClearAndResetTree() { if (_dispatcher is null) return Task.CompletedTask; @@ -479,7 +461,48 @@ partial void OnTreeDataSourceChanged(ObservableCollection? { rg.PropertyChanged -= KvResourceGroupNode_PropertyChanged; rg.PropertyChanged += KvResourceGroupNode_PropertyChanged; + + if (rg.IsExpanded) + _ = LoadResourceGroupVaults(rg); } } } + + private async Task LoadResourceGroupVaults(KvResourceGroupModel kvResourceModel) + { + var hasPlaceholder = kvResourceModel.KeyVaultResources.Any(k => k.GetType().Name == nameof(KeyVaultResourcePlaceholder)); + if (!kvResourceModel.IsExpanded || !hasPlaceholder) + return; + + kvResourceModel.KeyVaultResources.Clear(); + + try + { + await Task.Run(async () => + { + var vaults = _vaultService.GetKeyVaultsByResourceGroup(kvResourceModel.ResourceGroupResource); + var vaultsList = new List(); + + await foreach (var vault in vaults) + { + vaultsList.Add(vault); + } + + _dispatcher.TryEnqueue(() => + { + foreach (var vault in vaultsList) + { + kvResourceModel.KeyVaultResources.Add(vault); + } + }); + }); + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + Debug.WriteLine($"Error loading vaults for resource group {kvResourceModel.DisplayName}: {ex.Message}"); + } + } } From f7276b486b7c0061765c99f63bf2b562ac813bd0 Mon Sep 17 00:00:00 2001 From: cricketthomas Date: Tue, 24 Mar 2026 16:42:10 -0400 Subject: [PATCH 3/5] add more error handling --- .../UserControls/ViewModels/KeyVaultTreeViewModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs index fa7fe001..bfd92726 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs @@ -324,6 +324,7 @@ private Task ExecuteSearch(CancellationToken token) _dispatcher.TryEnqueue(() => { SelectedItem = null; + TreeDataSource = []; TreeDataSource = new ObservableCollection(TreeDataSourceReadOnly); }); return Task.CompletedTask; From e12a6338d444bf564534f3364c7b69f8253747be Mon Sep 17 00:00:00 2001 From: cricketthomas Date: Tue, 24 Mar 2026 17:15:46 -0400 Subject: [PATCH 4/5] revert due to many binding errors --- .../UserControls/KeyVaultTree.xaml.cs | 17 +++++++++++++++++ .../ViewModels/KeyVaultTreeViewModel.cs | 2 ++ 2 files changed, 19 insertions(+) diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs index bdc2b40d..83dc7db8 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs @@ -1,6 +1,7 @@ using System.Collections.Specialized; using System.ComponentModel; using Azure.ResourceManager.KeyVault; +using AzureKeyVaultStudio.Models; using AzureKeyVaultStudio.UserControls.ViewModels; using CommunityToolkit.WinUI; using Microsoft.UI.Dispatching; @@ -91,6 +92,22 @@ private void SubNodeCollectionChanged(object? sender, NotifyCollectionChangedEve private void ResourceGroupNodePropertyChanged(object? sender, PropertyChangedEventArgs e) { } + private void KeyVaultTreeView_Expanding(TreeView sender, TreeViewExpandingEventArgs args) + { + if (args.Item is KvSubscriptionModel sub) + sub.IsExpanded = true; + else if (args.Item is KvResourceGroupModel rg) + rg.IsExpanded = true; + } + + private void KeyVaultTreeView_Collapsed(TreeView sender, TreeViewCollapsedEventArgs args) + { + if (args.Item is KvSubscriptionModel sub) + sub.IsExpanded = false; + else if (args.Item is KvResourceGroupModel rg) + rg.IsExpanded = false; + } + private void KeyVaultTreeView_DragItemsStarting(TreeView sender, TreeViewDragItemsStartingEventArgs args) { args.Cancel = true; diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs index bfd92726..314f71cf 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/ViewModels/KeyVaultTreeViewModel.cs @@ -100,6 +100,7 @@ private async Task Refresh(CancellationToken token) #if DEBUG //await Task.Delay(4000, token); #endif + SearchQuery = string.Empty; await ClearAndResetTree(); await InitializeTreeDataSource(token); } @@ -414,6 +415,7 @@ private Task ClearAndResetTree() _dispatcher.TryEnqueue(() => { + SearchQuery = ""; SelectedItem = null; TreeDataSource = []; TreeDataSourceReadOnly = []; From b9293eefe9b8f5e45e8a623de37fa6d5c5179f63 Mon Sep 17 00:00:00 2001 From: cricketthomas Date: Tue, 24 Mar 2026 22:07:59 -0400 Subject: [PATCH 5/5] update to binding --- .../Models/KeyVaultModel.cs | 4 +- .../AzureKeyVaultStudio/Package.appxmanifest | 2 +- .../UserControls/KeyVaultTree.xaml | 60 +++++++------------ 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultModel.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultModel.cs index 52d77d76..a0710646 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultModel.cs +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Models/KeyVaultModel.cs @@ -23,6 +23,7 @@ public partial class PinnedItemModel : ObservableObject public partial class KvSubscriptionModel : ObservableObject { + [ObservableProperty] public partial bool HasSubNodeDataBeenFetched { get; set; } = false; @@ -39,7 +40,8 @@ public enum ExplorerItemType public ObservableCollection ResourceGroups { get; set; } = []; public virtual ObservableCollection PinnedItems { get; set; } = []; - + //adding this to avoid crashing the application. + public virtual ObservableCollection KeyVaultResources { get; set; } = []; public SubscriptionResource Subscription { get; set; } = null!; public string DisplayName { get; set; } = null!; public string? SubscriptionId { get; set; } diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Package.appxmanifest b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Package.appxmanifest index 3983b4e8..1bf48444 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Package.appxmanifest +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/Package.appxmanifest @@ -6,7 +6,7 @@ xmlns:uap18="http://schemas.microsoft.com/appx/manifest/uap/windows10/18" IgnorableNamespaces="uap rescap uap18"> - + Key Vault Explorer Arthur Thomas IV diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml index 0454d15b..a6a8fbcf 100644 --- a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml +++ b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml @@ -60,56 +60,58 @@ - + + + AutomationProperties.Name="{Binding DisplayName}" + IsExpanded="{Binding IsExpanded, Mode=TwoWay}" + IsSelected="{Binding IsSelected, Mode=TwoWay}" + ItemsSource="{Binding PinnedItems, Mode=OneWay}"> - + + - - + + AutomationProperties.Name="{Binding DisplayName}" + IsExpanded="{Binding IsExpanded, Mode=TwoWay}" + IsSelected="{Binding IsSelected, Mode=TwoWay}" + ItemsSource="{Binding ResourceGroups, Mode=OneWay}"> - + + - + + AutomationProperties.Name="{Binding DisplayName}" + IsExpanded="{Binding IsExpanded, Mode=TwoWay}" + IsSelected="{Binding IsSelected, Mode=TwoWay}" + ItemsSource="{Binding KeyVaultResources, Mode=OneWay}"> - + @@ -212,31 +214,13 @@ Modifiers="Control" /> - + Visibility="{x:Bind ViewModel.RefreshCommand.IsRunning, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=False}" />