Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

[Environments] Adding configuration flow #3492

Merged
merged 6 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DevHome.Common.Environments.Models;
Expand Down Expand Up @@ -98,7 +99,7 @@ public static async Task<List<PinOperationData>> FillDotButtonPinOperationsAsync
/// Returns the list of operations to be added to the launch button.
/// </summary>
// <param name="computeSystem">Compute system used to fill OperationsViewModel's callback function.</param>
public static List<OperationsViewModel> FillLaunchButtonOperations(ComputeSystemCache computeSystem)
public static List<OperationsViewModel> FillLaunchButtonOperations(ComputeSystemProvider provider, ComputeSystemCache computeSystem, Action<ComputeSystemReviewItem>? configurationCallback)
{
var operations = new List<OperationsViewModel>();
var supportedOperations = computeSystem.SupportedOperations.Value;
Expand Down Expand Up @@ -151,6 +152,12 @@ public static List<OperationsViewModel> FillLaunchButtonOperations(ComputeSystem
_stringResource.GetLocalized("Operations_Terminate"), "\uEE95", computeSystem.TerminateAsync, ComputeSystemOperations.Terminate));
}

if (supportedOperations.HasFlag(ComputeSystemOperations.ApplyConfiguration) && configurationCallback is not null)
{
operations.Add(new OperationsViewModel(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comments in ComputeSystemViewModel.cs, I don't think we'll need to update the DataExtractor code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

_stringResource.GetLocalized("Operations_ApplyConfiguration"), "\uE835", configurationCallback, provider, computeSystem));
}

return operations;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -358,5 +358,8 @@
<data name="MoreOptionsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>More options</value>
<comment>Name of the button that brings up the "More options" menu</comment>
<data name="Operations_ApplyConfiguration" xml:space="preserve">
<value>Setup</value>
<comment>Value to be shown in the button option to start the configuration flow</comment>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public partial class ComputeSystemViewModel : ComputeSystemCardBase, IRecipient<

private bool _disposedValue;

private Action<ComputeSystemReviewItem>? _configurationAction;

/// <summary>
/// Initializes a new instance of the <see cref="ComputeSystemViewModel"/> class.
/// This class requires a 3-step initialization:
Expand All @@ -73,6 +75,7 @@ public ComputeSystemViewModel(
IComputeSystem system,
ComputeSystemProvider provider,
Func<ComputeSystemCardBase, bool> removalAction,
Action<ComputeSystemReviewItem>? configurationAction,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this constructor is getting pretty big, we should combine some of these parameters into objects. E.g we can have an object to contain the actions like:

// In a new class called DevHomeUIActions
public Func<ComputeSystemCardBase, bool> RemoveComputeSystem;

public Action<ComputeSystemReviewItem>? StartComputeSystemSetup;

public DevHomeUIActions(
    Func<ComputeSystemCardBase, bool> removalAction,
    Action<ComputeSystemReviewItem>? startConfigurationFlowAction)
{
    // add to public properties....
    RemoveComputeSystem = removalAction;
    StartComputeSystemSetup = startConfigurationFlowAction
}
///// DevHomeUIActions end

// Then use it as a parameter for the `ComputeSystemViewModel`'s constructor
private readonly ComputeSystemReviewItem _reviewItem;

private readonly DevHomeUIActions _uiActions;

public ComputeSystemViewModel(
        IComputeSystemManager manager,
        IComputeSystem system,
        ComputeSystemProvider provider,
        DevHomeUIActions uiActions,
        string packageFullName,
        Window window)
    {
        ...
        ComputeSystem = new(system);
        _uiActions = uiActions;
        _reviewItem = new (ComputeSystem , provider);
        ...
    }

If we do it this way, we don't need to add the new field above (_configurationAction) on line 61 in this PR. As it'll be in the UIActions property. Your codeflow will remain the same, this just helps keep the constructor more manageable. In the future we can reduce the number of parameters further by doing more consolidation, but I think for what we're doing in this PR, this should suffice.

Since we know the provider and the ComputeSystemCache are available in the constructor, we can also create the ComputeSystemReviewItem directly in the constructor. See comment below on how we can use it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gonna skip this for a later PR that might add more; quite a few examples with 7 or more parameters in the constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding my 2-cents. A ctor with many parameters tells me that this class is trying to do too many things. I wonder if this class can be split? No need to respond to this comment.

string packageFullName,
Window window)
{
Expand All @@ -83,6 +86,7 @@ public ComputeSystemViewModel(
ComputeSystem = new(system);
PackageFullName = packageFullName;
_removalAction = removalAction;
_configurationAction = configurationAction;
_stringResource = new StringResource("DevHome.Environments.pri", "DevHome.Environments/Resources");
}

Expand Down Expand Up @@ -133,7 +137,7 @@ private async Task InitializeOperationDataAsync()
ShouldShowDotOperations = false;
ShouldShowSplitButton = false;

RegisterForAllOperationMessages(DataExtractor.FillDotButtonOperations(ComputeSystem, _mainWindow), DataExtractor.FillLaunchButtonOperations(ComputeSystem));
RegisterForAllOperationMessages(DataExtractor.FillDotButtonOperations(ComputeSystem, _mainWindow), DataExtractor.FillLaunchButtonOperations(_provider, ComputeSystem, _configurationAction));
Copy link
Contributor

@bbonaby bbonaby Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the above changes we can then simplify what you did in the DataExtractor. We can have a method in this ComputeSystemViewModel class like:

private void AddStartSetupFlowOperation()
{
    var operations = ComputeSystem.SupportedOperations.Value;
    if (!operations .HasFlag(ComputeSystemOperations.ApplyConfiguration) || 
        _uiActions.StartComputeSystemSetup == null))
    {
        return;
    }

    // Use lambda to create Action that does not take a parameter
    var configurationAction = () =>
    { 
          _uiActions.StartComputeSystemSetup(_reviewItem)
    };

    LaunchOperations.Add(new OperationsViewModel(
        _stringResource.GetLocalized("Operations_ApplyConfiguration"), "\uE835", configurationAction));
}

This can be called right after this FillLaunchButtonOperations call without the need to pass the provider or the action to the DataExtractor. This way you don't need to update code in the DataExtractor or OperationsViewModel. We'd just be using the constructor for OperationsViewModel that already exists which takes in an Action.

We'll end up with something like:

RegisterForAllOperationMessages(DataExtractor.FillDotButtonOperations(ComputeSystem, _mainWindow), DataExtractor.FillLaunchButtonOperations(ComputeSystem));
AddStartSetupFlowOperation();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.


_ = Task.Run(async () =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ private async void AddComputeSystemToUI(CreateComputeSystemResult result)
result.ComputeSystem,
Operation.ProviderDetails.ComputeSystemProvider,
_removalAction,
null,
Operation.ProviderDetails.ExtensionWrapper.PackageFullName,
_mainWindow);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ public void CallToActionInvokeButton()
_navigationService.NavigateTo(KnownPageKeys.SetupFlow, "startCreationFlow;EnvironmentsLandingPage");
}

public void ConfigureComputeSystem(ComputeSystemReviewItem item)
{
_log.Information("User clicked on the setup button. Navigating to the Setup an Environment page in Setup flow");
object[] parameters = { "StartConfigurationFlow", "EnvironmentsLandingPage", item };

// Run on the UI thread
_mainWindow.DispatcherQueue.EnqueueAsync(() =>
{
_navigationService.NavigateTo(KnownPageKeys.SetupFlow, parameters);
});
}

// Updates the last sync time on the UI thread after set delay
private async Task UpdateLastSyncTimeUI(string time, TimeSpan delay, CancellationToken token)
{
Expand Down Expand Up @@ -337,6 +349,7 @@ private async Task AddAllComputeSystemsFromAProvider(ComputeSystemsLoadedData da
computeSystem,
provider,
RemoveComputeSystemCard,
ConfigureComputeSystem,
packageFullName,
_mainWindow);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments in ComputeSystemViewModel.cs file as I believe with those changes we won't need to make any changes to this file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

using DevHome.Common.Environments.Models;
using DevHome.Common.Services;
using DevHome.Environments.Models;
using Microsoft.UI.Xaml;
Expand Down Expand Up @@ -37,6 +38,10 @@ public partial class OperationsViewModel : IEquatable<OperationsViewModel>

private readonly string _additionalContext = string.Empty;

private readonly Window? _mainWindow;

private readonly StringResource _stringResource = new("DevHome.Environments.pri", "DevHome.Environments/Resources");

public string Name { get; }

public ComputeSystemOperations ComputeSystemOperation { get; }
Expand All @@ -47,9 +52,9 @@ public partial class OperationsViewModel : IEquatable<OperationsViewModel>

private Action? DevHomeAction { get; }

private readonly Window? _mainWindow;
private Action<ComputeSystemReviewItem>? DevHomeActionWithReviewItem { get; }

private readonly StringResource _stringResource = new("DevHome.Environments.pri", "DevHome.Environments/Resources");
private ComputeSystemReviewItem? _item;

public OperationsViewModel(
string name,
Expand Down Expand Up @@ -89,14 +94,36 @@ public OperationsViewModel(string name, string icon, Action command)
DevHomeAction = command;
}

public OperationsViewModel(
string name,
string icon,
Action<ComputeSystemReviewItem>? command,
ComputeSystemProvider provider,
ComputeSystemCache cache)
{
_operationKind = OperationKind.DevHomeAction;
Name = name;
IconGlyph = icon;
DevHomeActionWithReviewItem = command;
_item = new(cache, provider);
}

private void RunAction()
{
// To Do: Need to disable the card UI while the operation is in progress and handle failures.
Task.Run(async () =>
{
if (_operationKind == OperationKind.DevHomeAction)
{
DevHomeAction!();
if (DevHomeAction != null)
{
DevHomeAction();
}
else if (DevHomeActionWithReviewItem != null && _item != null)
{
DevHomeActionWithReviewItem(_item);
}

return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using DevHome.Common.Environments.Models;
using DevHome.Common.Environments.Services;
using DevHome.Common.Extensions;
using DevHome.Common.Services;
Expand Down Expand Up @@ -382,6 +383,11 @@ public void RemoveAdaptiveCardPanelFromLoadingUI()
});
}

public void SetTargetComputeSystem(ComputeSystemReviewItem item)
{
_computeSystemManager.ComputeSystemSetupItem = item;
}

public IAsyncOperation<TaskFinishedState> Execute()
{
return Task.Run(async () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace DevHome.SetupFlow.TaskGroups;
public class SetupTargetTaskGroup : ISetupTaskGroup
{
private readonly SetupTargetViewModel _setupTargetViewModel;

private readonly SetupTargetReviewViewModel _setupTargetReviewViewModel;

private readonly ConfigureTargetTask _setupTargetTaskGroup;
Expand Down
20 changes: 20 additions & 0 deletions tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DevHome.Common.Environments.Models;
using DevHome.Common.Extensions;
using DevHome.Common.Models;
using DevHome.Common.Services;
Expand Down Expand Up @@ -226,6 +227,25 @@ private void StartSetupForTargetEnvironment(string flowTitle)
_host.GetService<AppManagementTaskGroup>());
}

/// <summary>
/// Starts the setup target flow from the environments page.
/// </summary>
public void StartSetupForTargetEnvironmentWithTelemetry(string flowTitle, string navigationAction, string originPage, ComputeSystemReviewItem item)
{
var setupTask = _host.GetService<SetupTargetTaskGroup>();

_log.Information("Starting setup for target environment from the Environments page");
StartSetupFlowForTaskGroups(
flowTitle,
"SetupTargetEnvironment",
setupTask,
_host.GetService<RepoConfigTaskGroup>(),
_host.GetService<AppManagementTaskGroup>());

setupTask.ConfigureTask.SetTargetComputeSystem(item);
Orchestrator.GoToNextPage().GetAwaiter().GetResult();
}

/// <summary>
/// Starts a setup flow that only includes repo config.
/// </summary>
Expand Down
49 changes: 37 additions & 12 deletions tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DevHome.Common.Environments.Models;
using DevHome.Common.Extensions;
using DevHome.Common.Services;
using DevHome.Common.TelemetryEvents.SetupFlow;
Expand All @@ -29,6 +30,8 @@ public partial class SetupFlowViewModel : ObservableObject

private readonly string _creationFlowNavigationParameter = "StartCreationFlow";

private readonly string _configurationFlowNavigationParameter = "StartConfigurationFlow";

public SetupFlowOrchestrator Orchestrator { get; }

public event EventHandler EndSetupFlow = (s, e) => { };
Expand Down Expand Up @@ -123,30 +126,52 @@ public async Task StartFileActivationFlowAsync(StorageFile file)
await _mainPageViewModel.StartConfigurationFileAsync(file);
}

public void StartCreationFlowAsync(string originPage)
public void StartCreationFlow(string originPage)
{
Orchestrator.FlowPages = [_mainPageViewModel];

// This method is only called when the user clicks a button that redirects them to 'Create Environment' flow in the setup flow.
_mainPageViewModel.StartCreateEnvironmentWithTelemetry(string.Empty, _creationFlowNavigationParameter, originPage);
}

public void StartSetupFlow(string originPage, ComputeSystemReviewItem item)
{
Orchestrator.FlowPages = [_mainPageViewModel];

// This method is only called when the user clicks a button that redirects them to 'Setup' flow in the Environments page.
_mainPageViewModel.StartSetupForTargetEnvironmentWithTelemetry(string.Empty, _configurationFlowNavigationParameter, originPage, item);
}

public void OnNavigatedTo(NavigationEventArgs args)
{
// The setup flow isn't setup to support using the navigation service to navigate to specific
// pages. Instead we need to navigate to the main page and then start the creation flow template manually.
var parameter = args.Parameter?.ToString();

if ((!string.IsNullOrEmpty(parameter)) &&
parameter.Contains(_creationFlowNavigationParameter, StringComparison.OrdinalIgnoreCase) &&
Orchestrator.CurrentSetupFlowKind != SetupFlowKind.CreateEnvironment)
if (args.Parameter is object[] parameters && parameters.Length == 3)
{
// We expect that when navigating from anywhere in Dev Home to the create environment page
// that the arg.Parameter variable be semicolon delimited string with the first value being 'StartCreationFlow'
// and the second value being the page name that redirection came from for telemetry purposes.
var parameters = parameter.Split(';');
Cancel();
StartCreationFlowAsync(originPage: parameters[1]);
if (parameters[0] is string parameter && parameter.Equals(_configurationFlowNavigationParameter, StringComparison.OrdinalIgnoreCase))
{
// We expect that when navigating from anywhere in Dev Home to the create environment page
// that the arg.Parameter variable be an object array with the the first value being 'StartCreationFlow',
// the second value being the page name that redirection came from for telemetry purposes, and
// the third value being the ComputeSystemReviewItem to setup.
Cancel();
StartSetupFlow(originPage: parameters[1] as string, item: parameters[2] as ComputeSystemReviewItem);
}
}
else
{
var parameter = args.Parameter?.ToString();
if (!string.IsNullOrEmpty(parameter) && Orchestrator.CurrentSetupFlowKind != SetupFlowKind.CreateEnvironment)
{
if (parameter.Contains(_creationFlowNavigationParameter, StringComparison.OrdinalIgnoreCase))
{
// We expect that when navigating from anywhere in Dev Home to the create environment page
// that the arg.Parameter variable be semicolon delimited string with the first value being 'StartCreationFlow'
// and the second value being the page name that redirection came from for telemetry purposes.
Cancel();
StartCreationFlow(originPage: parameter.Split(';')[1]);
}
}
}
}

Expand Down
Loading