diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/App.xaml.cs index 876bc9c9..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}"); @@ -152,7 +160,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..a0710646 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; @@ -22,6 +23,7 @@ public partial class PinnedItemModel : ObservableObject public partial class KvSubscriptionModel : ObservableObject { + [ObservableProperty] public partial bool HasSubNodeDataBeenFetched { get; set; } = false; @@ -38,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; } @@ -67,19 +70,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/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/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..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}" /> diff --git a/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs b/src/uno/AzureKeyVaultStudio/AzureKeyVaultStudio/UserControls/KeyVaultTree.xaml.cs index 364ca509..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; @@ -39,7 +40,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 +92,25 @@ 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; + 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; @@ -68,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 @@ -100,12 +100,17 @@ private async Task Refresh(CancellationToken token) #if DEBUG //await Task.Delay(4000, token); #endif - _ = Task.Run(() => InitializeTreeDataSource(token), token); + SearchQuery = string.Empty; + 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; @@ -124,9 +129,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 +159,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 +180,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 +195,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) @@ -236,31 +238,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); } } @@ -315,7 +293,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); } @@ -336,24 +314,35 @@ 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 = []; + 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] @@ -398,6 +387,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) @@ -413,4 +405,107 @@ internal void SetDispatcher(IDispatcher dispatcher) { _dispatcher = dispatcher; } + + private Task ClearAndResetTree() + { + if (_dispatcher is null) + return Task.CompletedTask; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _dispatcher.TryEnqueue(() => + { + SearchQuery = ""; + 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; + + 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}"); + } + } }