sources/Google.Solutions.IapDesktop.Application/ToolWindows/ProjectExplorer/ProjectExplorerViewModel.cs (389 lines of code) (raw):

// // Copyright 2021 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Apis.Locator; using Google.Solutions.Common.Runtime; using Google.Solutions.IapDesktop.Application.Client; using Google.Solutions.IapDesktop.Application.Profile.Settings; using Google.Solutions.IapDesktop.Application.Windows; using Google.Solutions.IapDesktop.Core.ObjectModel; using Google.Solutions.IapDesktop.Core.ProjectModel; using Google.Solutions.Mvvm.Binding; using Google.Solutions.Mvvm.Binding.Commands; using Google.Solutions.Settings.Collection; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Google.Solutions.IapDesktop.Application.ToolWindows.ProjectExplorer { public partial class ProjectExplorerViewModel : ViewModelBase, IDisposable { private readonly IJobService jobService; private readonly IProjectWorkspace workspace; private readonly ISessionBroker sessionBroker; private readonly ICloudConsoleClient cloudConsoleService; private ViewModelNode? selectedNode; private string? instanceFilter; private OperatingSystems operatingSystemsFilter = OperatingSystems.All; private readonly IProjectExplorerSettings settings; private bool isUnloadProjectCommandVisible; private bool isRefreshProjectsCommandVisible; private bool isRefreshAllProjectsCommandVisible; private bool isCloudConsoleCommandVisible; private bool isLoading = false; private IDisposable EnableLoadingStatus() { this.IsLoading = true; return Disposable.Create(() => this.IsLoading = false); } private static async Task RefreshAsync(ViewModelNode node) { if (!node.CanReload) { Debug.Assert(node.Parent != null); if (node.Parent != null) { // // Try reloading parent instead. // await RefreshAsync(node.Parent) .ConfigureAwait(true); } else { // // Ignore. // } } else { // Force-reload children and discard result. await node.GetFilteredChildrenAsync(true) .ConfigureAwait(true); } } internal ProjectExplorerViewModel( IProjectExplorerSettings settings, IJobService jobService, IEventQueue eventQueue, ISessionBroker sessionBroker, IProjectWorkspace workspace, ICloudConsoleClient cloudConsoleService) { this.settings = settings; this.jobService = jobService; this.sessionBroker = sessionBroker; this.workspace = workspace; this.cloudConsoleService = cloudConsoleService; this.RefreshSelectedNodeCommand = ObservableCommand.Build( "Refresh", RefreshSelectedNodeAsync); this.RootNode = new CloudViewModelNode(this); // // NB. Only consider instances that have already bee loaded. // eventQueue.Subscribe<SessionStartedEvent>( e => { if (FindLoadedInstance(e.Instance) is InstanceViewModelNode instance) { instance.IsConnected = true; } }); eventQueue.Subscribe<SessionEndedEvent>( e => { if (FindLoadedInstance(e.Instance) is InstanceViewModelNode instance) { instance.IsConnected = false; } }); eventQueue.Subscribe<InstanceStateChangedEvent>( async e => { // // Refresh the instance node (or rather, its enclosing project) // to ensure that both the UI and the underlying project model // updated. // if (FindLoadedInstance(e.Instance) is InstanceViewModelNode instance) { await RefreshAsync(instance).ConfigureAwait(true); } }); } public ProjectExplorerViewModel( IRepository<IApplicationSettings> settingsRepository, IJobService jobService, IEventQueue eventService, ISessionBroker sessionBroker, IProjectWorkspace workspace, ICloudConsoleClient cloudConsoleService) : this( new ProjectExplorerSettings( settingsRepository, true), jobService, eventService, sessionBroker, workspace, cloudConsoleService) { } /// <summary> /// Look for instance among the loaded nodes. /// </summary> private InstanceViewModelNode? FindLoadedInstance( InstanceLocator locator) { return this.RootNode.LoadedDescendents .OfType<InstanceViewModelNode>() .FirstOrDefault(i => Equals(i.Locator, locator)); } internal IReadOnlyCollection<IProjectModelProjectNode> Projects { get { if (!this.RootNode.IsLoaded) { return Array.Empty<IProjectModelProjectNode>(); } else { var modelNode = (IProjectModelCloudNode)this.RootNode.ModelNode; return modelNode.Projects.ToList(); } } } //--------------------------------------------------------------------- // Commands. //--------------------------------------------------------------------- public ObservableCommand RefreshSelectedNodeCommand { get; } //--------------------------------------------------------------------- // Observable properties. //--------------------------------------------------------------------- public bool IsLoading { get => this.isLoading; set { this.isLoading = value; RaisePropertyChange(); } } public bool IsLinuxIncluded { get => this.OperatingSystemsFilter.HasFlag(OperatingSystems.Linux); set { if (value) { this.OperatingSystemsFilter |= OperatingSystems.Linux; } else { this.OperatingSystemsFilter &= ~OperatingSystems.Linux; } RaisePropertyChange(); } } public bool IsWindowsIncluded { get => this.OperatingSystemsFilter.HasFlag(OperatingSystems.Windows); set { if (value) { this.OperatingSystemsFilter |= OperatingSystems.Windows; } else { this.OperatingSystemsFilter &= ~OperatingSystems.Windows; } RaisePropertyChange(); } } public CloudViewModelNode RootNode { get; } public bool IsUnloadProjectCommandVisible { get => this.isUnloadProjectCommandVisible; set { this.isUnloadProjectCommandVisible = value; RaisePropertyChange(); } } public bool IsRefreshProjectsCommandVisible { get => this.isRefreshProjectsCommandVisible; set { this.isRefreshProjectsCommandVisible = value; RaisePropertyChange(); } } public bool IsRefreshAllProjectsCommandVisible { get => this.isRefreshAllProjectsCommandVisible; set { this.isRefreshAllProjectsCommandVisible = value; RaisePropertyChange(); } } public bool IsCloudConsoleCommandVisible { get => this.isCloudConsoleCommandVisible; set { this.isCloudConsoleCommandVisible = value; RaisePropertyChange(); } } //--------------------------------------------------------------------- // "Input" properties. //--------------------------------------------------------------------- public string? InstanceFilter { get => this.instanceFilter?.Trim(); set { this.instanceFilter = value; RaisePropertyChange(); if (this.RootNode.IsLoaded) { this.RootNode.ReapplyFilter(); } } } public OperatingSystems OperatingSystemsFilter { get => this.operatingSystemsFilter; set { this.operatingSystemsFilter = value; RaisePropertyChange(); if (this.RootNode.IsLoaded) { this.RootNode.ReapplyFilter(); } } } public ViewModelNode? SelectedNode { get { Debug.Assert( this.selectedNode == null || this.RootNode.DebugIsValidNode(this.selectedNode), "Node detached"); if (this.selectedNode is CloudViewModelNode cloudNode && !cloudNode.IsLoaded) { // // Not fully initialized yet. // return null; } return this.selectedNode; } set { Debug.Assert( this.selectedNode == null || this.RootNode.DebugIsValidNode(this.selectedNode), "Node detached"); this.IsUnloadProjectCommandVisible = value is ProjectViewModelNode; this.IsRefreshAllProjectsCommandVisible = value is CloudViewModelNode; this.IsRefreshProjectsCommandVisible = value is ProjectViewModelNode || value is ZoneViewModelNode || value is InstanceViewModelNode; this.IsCloudConsoleCommandVisible = value is ProjectViewModelNode || value is ZoneViewModelNode || value is InstanceViewModelNode; this.selectedNode = value; RaisePropertyChange(); // // Update active node in model. // _ = this.workspace.SetActiveNodeAsync( value?.Locator, CancellationToken.None); } } //--------------------------------------------------------------------- // Actions. //--------------------------------------------------------------------- public async Task AddProjectsAsync(params ProjectLocator[] projects) { foreach (var project in projects) { await this.workspace .AddProjectAsync(project) .ConfigureAwait(true); } // // Refresh to ensure the new project is reflected. // await RefreshAsync(true).ConfigureAwait(true); } public async Task RemoveProjectsAsync(params ProjectLocator[] projects) { // // Reset selection to a safe place. // this.SelectedNode = this.RootNode; foreach (var project in projects) { await this.workspace .RemoveProjectAsync(project) .ConfigureAwait(true); // // Remove from collapsed list so that we don't // accumulate junk. // this.settings.CollapsedProjects.Remove(project); } // // Refresh to ensure the removal is reflected. // await RefreshAsync(true).ConfigureAwait(true); } public async Task<IEnumerable<ViewModelNode>> ExpandRootAsync() { // Explicitly load nodes. var nodes = await this.RootNode.GetFilteredChildrenAsync(false) .ConfigureAwait(true); // NB. If we did not load the nodes explicitly before, // IsExpanded would asynchronously trigger a load without // awaiting the result. To prevent this behavior. this.RootNode.IsExpanded = true; return nodes; } public async Task RefreshAsync(bool reloadProjects) { // Reset selection to a safe place. this.SelectedNode = this.RootNode; if (reloadProjects) { // Refresh everything. await RefreshAsync(this.RootNode).ConfigureAwait(true); } else { // Retain project nodes, but refresh their descendents. var projects = await this.RootNode .GetFilteredChildrenAsync(false) .ConfigureAwait(true); await Task .WhenAll(projects .Where(p => p.IsLoaded && p.CanReload) .Select(p => p.GetFilteredChildrenAsync(true))) .ConfigureAwait(true); } } public async Task RefreshSelectedNodeAsync() { await RefreshAsync(this.SelectedNode ?? this.RootNode) .ConfigureAwait(true); } public async Task UnloadSelectedProjectAsync() { if (this.SelectedNode is ProjectViewModelNode projectNode) { await RemoveProjectsAsync(projectNode.ProjectNode.Project) .ConfigureAwait(true); } } public void OpenInCloudConsole() { if (this.SelectedNode is InstanceViewModelNode vmInstanceNode) { this.cloudConsoleService.OpenInstanceDetails( vmInstanceNode.InstanceNode.Instance); } else if (this.SelectedNode is ZoneViewModelNode zoneNode) { this.cloudConsoleService.OpenInstanceList( zoneNode.ZoneNode.Zone); } else if (this.SelectedNode is ProjectViewModelNode projectNode) { this.cloudConsoleService.OpenInstanceList( projectNode.ProjectNode.Project); } } public void ConfigureIapAccess() { if (this.SelectedNode is InstanceViewModelNode vmInstanceNode) { this.cloudConsoleService.OpenIapSecurity( vmInstanceNode.InstanceNode.Instance.ProjectId); } else if (this.SelectedNode is ZoneViewModelNode zoneNode) { this.cloudConsoleService.OpenIapSecurity( zoneNode.ZoneNode.Zone.ProjectId); } else if (this.SelectedNode is ProjectViewModelNode projectNode) { this.cloudConsoleService.OpenIapSecurity( projectNode.ProjectNode.Project.Name); } } //--------------------------------------------------------------------- // IDisposable. //--------------------------------------------------------------------- protected override void Dispose(bool disposing) { base.Dispose(disposing); this.settings.Dispose(); this.workspace.Dispose(); } } }