diff --git a/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepError.cs b/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepError.cs new file mode 100644 index 000000000..dfa87c6a6 --- /dev/null +++ b/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepError.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.Common.TelemetryEvents.SetupFlow.SummaryPage; + +[EventData] +public class CloneRepoNextStepError : EventBase +{ + public string Operation { get; } + + public string ErrorMessage { get; } + + public string RepoName { get; } + + public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServicePerformance; + + public CloneRepoNextStepError(string operation, string errorMessage, string repoName) + { + Operation = operation; + ErrorMessage = errorMessage; + RepoName = repoName; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // no op + } +} diff --git a/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepEvent.cs b/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepEvent.cs new file mode 100644 index 000000000..6d0017592 --- /dev/null +++ b/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepEvent.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.Tracing; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.Common.TelemetryEvents.SetupFlow.SummaryPage; + +[EventData] +public class CloneRepoNextStepEvent : EventBase +{ + public string Operation { get; } + + public string RepoName { get; } + + public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServicePerformance; + + public CloneRepoNextStepEvent(string operation, string repoName) + { + Operation = operation; + RepoName = repoName; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // no op + } +} diff --git a/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepsEvent.cs b/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepsEvent.cs new file mode 100644 index 000000000..5882d8d48 --- /dev/null +++ b/common/TelemetryEvents/SetupFlow/SummaryPage/CloneRepoNextStepsEvent.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DevHome.Telemetry; +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; + +namespace DevHome.Common.TelemetryEvents.SetupFlow.SummaryPage; + +[EventData] +public class CloneRepoNextStepsEvent : EventBase +{ + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServicePerformance; + + public int NumberOfConfigurationFilesFound { get; } + + public CloneRepoNextStepsEvent(int numberOfConfigurationFilesFound) + { + NumberOfConfigurationFilesFound = numberOfConfigurationFilesFound; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No op. + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Helpers/DscHelpers.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Helpers/DscHelpers.cs index 4519578d2..ed255d617 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/Helpers/DscHelpers.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Helpers/DscHelpers.cs @@ -24,5 +24,11 @@ public static class DscHelpers public const string DevHomeHeaderBanner = @"# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2 # Reference: https://github.com/microsoft/winget-create#building-the-client -# WinGet Configure file Generated By Dev Home."; +# WinGet Configure file Generated By Dev Home."; + + public const string ConfigurationFolderName = ".configurations"; + + public const string ConfigurationFileYamlExtension = ".dsc.yaml"; + + public const string ConfigurationFileWingetExtension = ".winget"; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj index b238cc05a..079631ccd 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj @@ -45,6 +45,9 @@ Always + + MSBuild:Compile + MSBuild:Compile @@ -87,6 +90,7 @@ + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs b/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs index 1b906a46c..d59e104b7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs @@ -32,6 +32,7 @@ public static IServiceCollection AddSetupFlow(this IServiceCollection services, services.AddRepoConfig(); services.AddReview(); services.AddSummary(); + services.AddSummaryInformation(); // View-models services.AddSingleton(); @@ -114,6 +115,14 @@ private static IServiceCollection AddConfigurationFile(this IServiceCollection s return services; } + private static IServiceCollection AddSummaryInformation(this IServiceCollection services) + { + // View models + services.AddTransient(); + + return services; + } + private static IServiceCollection AddDevDrive(this IServiceCollection services) { // View models @@ -133,6 +142,7 @@ private static IServiceCollection AddLoading(this IServiceCollection services) { // View models services.AddTransient(); + services.AddTransient(); return services; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs index c16a01d92..0a9e2f085 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs @@ -6,14 +6,20 @@ using System; using System.Globalization; using System.IO; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.Extensions; using DevHome.Common.Services; using DevHome.Common.TelemetryEvents; using DevHome.Common.TelemetryEvents.SetupFlow; +using DevHome.SetupFlow.Common; +using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; using DevHome.Telemetry; +using Microsoft.Extensions.Hosting; using Microsoft.Windows.DevHome.SDK; using Projection::DevHome.SetupFlow.ElevatedComponent; using Serilog; @@ -27,6 +33,8 @@ namespace DevHome.SetupFlow.Models; /// public partial class CloneRepoTask : ObservableObject, ISetupTask { + private readonly IHost _host; + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CloneRepoTask)); private readonly Guid _activityId; @@ -131,13 +139,17 @@ public bool DependsOnDevDriveToBeInstalled get; set; } + private readonly CloneRepoSummaryInformationViewModel _summaryScreenInformation; + + public ISummaryInformationViewModel SummaryScreenInformation => _summaryScreenInformation; + /// /// Initializes a new instance of the class. /// /// Repository will be placed here. at _cloneLocation.FullName /// The repository to clone /// Credentials needed to clone a private repo - public CloneRepoTask(IRepositoryProvider repositoryProvider, DirectoryInfo cloneLocation, IRepository repositoryToClone, IDeveloperId developerId, IStringResource stringResource, string providerName, Guid activityId) + public CloneRepoTask(IRepositoryProvider repositoryProvider, DirectoryInfo cloneLocation, IRepository repositoryToClone, IDeveloperId developerId, ISetupFlowStringResource stringResource, string providerName, Guid activityId, IHost host) { _cloneLocation = cloneLocation; this.RepositoryToClone = repositoryToClone; @@ -147,6 +159,8 @@ public CloneRepoTask(IRepositoryProvider repositoryProvider, DirectoryInfo clone _stringResource = stringResource; _repositoryProvider = repositoryProvider; _activityId = activityId; + _host = host; + _summaryScreenInformation = new CloneRepoSummaryInformationViewModel(host.GetService(), stringResource); } /// @@ -155,7 +169,7 @@ public CloneRepoTask(IRepositoryProvider repositoryProvider, DirectoryInfo clone /// /// Repository will be placed here, at _cloneLocation.FullName /// The repository to clone - public CloneRepoTask(IRepositoryProvider repositoryProvider, DirectoryInfo cloneLocation, IRepository repositoryToClone, IStringResource stringResource, string providerName, Guid activityId) + public CloneRepoTask(IRepositoryProvider repositoryProvider, DirectoryInfo cloneLocation, IRepository repositoryToClone, ISetupFlowStringResource stringResource, string providerName, Guid activityId, IHost host) { _cloneLocation = cloneLocation; this.RepositoryToClone = repositoryToClone; @@ -165,6 +179,8 @@ public CloneRepoTask(IRepositoryProvider repositoryProvider, DirectoryInfo clone _stringResource = stringResource; _repositoryProvider = repositoryProvider; _activityId = activityId; + _host = host; + _summaryScreenInformation = new CloneRepoSummaryInformationViewModel(host.GetService(), stringResource); } private void SetMessages(IStringResource stringResource) @@ -232,7 +248,26 @@ IAsyncOperation ISetupTask.Execute() return TaskFinishedState.Failure; } + // Search for a configuration file. + var configurationDirectory = Path.Join(_cloneLocation.FullName, DscHelpers.ConfigurationFolderName); + if (Directory.Exists(configurationDirectory)) + { + var fileToUse = Directory.EnumerateFiles(configurationDirectory) + .Where(file => file.EndsWith(DscHelpers.ConfigurationFileYamlExtension, StringComparison.OrdinalIgnoreCase) || + file.EndsWith(DscHelpers.ConfigurationFileWingetExtension, StringComparison.OrdinalIgnoreCase)) + .OrderByDescending(configurationFile => File.GetLastWriteTime(configurationFile)) + .FirstOrDefault(); + + if (fileToUse != null) + { + _summaryScreenInformation.FilePathAndName = fileToUse; + _summaryScreenInformation.RepoName = RepositoryName; + _summaryScreenInformation.OwningAccount = RepositoryToClone.OwningAccountName ?? string.Empty; + } + } + WasCloningSuccessful = true; + return TaskFinishedState.Success; }).AsAsyncOperation(); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs index e29ff3a9c..7216c5dcf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs @@ -16,7 +16,8 @@ using DevHome.SetupFlow.Common.Exceptions; using DevHome.SetupFlow.Exceptions; using DevHome.SetupFlow.Models.WingetConfigure; -using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; using Microsoft.UI.Xaml; using Microsoft.Windows.DevHome.SDK; using Projection::DevHome.SetupFlow.ElevatedComponent; @@ -93,7 +94,9 @@ public class ConfigureTargetTask : ISetupTask public SDKApplyConfigurationResult Result { get; private set; } public IAsyncOperation ApplyConfigurationAsyncOperation { get; private set; } - + + public ISummaryInformationViewModel SummaryScreenInformation { get; } + public ConfigureTargetTask( ISetupFlowStringResource stringResource, IComputeSystemManager computeSystemManager, @@ -155,8 +158,8 @@ public void OnActionRequired(IApplyConfigurationOperation operation, SDK.ApplyCo ExtensionAdaptiveCardSession.Stopped += OnAdaptiveCardSessionStopped; CreateCorrectiveActionPanel(ExtensionAdaptiveCardSession).GetAwaiter().GetResult(); - - AddMessage(_stringResource.GetLocalized(StringResourceKey.ConfigureTargetApplyConfigurationActionNeeded, UserNumberOfAttempts++), MessageSeverityKind.Warning); + + AddMessage(_stringResource.GetLocalized(StringResourceKey.ConfigureTargetApplyConfigurationActionNeeded, UserNumberOfAttempts++, UserMaxNumberOfAttempts), MessageSeverityKind.Warning); } else { diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs index 2a138494c..2235ebeea 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using DevHome.SetupFlow.Common.Contracts; using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; using Projection::DevHome.SetupFlow.ElevatedComponent; using Serilog; using Windows.Foundation; @@ -44,6 +45,8 @@ public IList UnitResults get; private set; } + public ISummaryInformationViewModel SummaryScreenInformation { get; } + public ConfigureTask( ISetupFlowStringResource stringResource, IDesiredStateConfiguration dsc, diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs index 9511ee03c..cd833b45a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs @@ -16,6 +16,7 @@ using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Common.TelemetryEvents; using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; using DevHome.Telemetry; using Microsoft.Extensions.Hosting; using Projection::DevHome.SetupFlow.ElevatedComponent; @@ -50,6 +51,8 @@ public IDevDrive DevDrive get; set; } + public ISummaryInformationViewModel SummaryScreenInformation { get; } + public CreateDevDriveTask(IDevDrive devDrive, IHost host, Guid activityId, ISetupFlowStringResource stringResource) { DevDrive = devDrive; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTask.cs index f774d6e77..7fc8c9715 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTask.cs @@ -3,6 +3,7 @@ extern alias Projection; +using DevHome.SetupFlow.ViewModels; using Projection::DevHome.SetupFlow.ElevatedComponent; using Windows.Foundation; @@ -113,6 +114,8 @@ public abstract bool DependsOnDevDriveToBeInstalled /// public event ChangeMessageHandler AddMessage; + public ISummaryInformationViewModel SummaryScreenInformation { get; } + public delegate void ChangeActionCenterMessageHandler(ActionCenterMessages message, ActionMessageRequestKind requestKind); /// diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs index c9eee3c9e..be88f85e7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs @@ -10,6 +10,7 @@ using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Exceptions; using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; using DevHome.Telemetry; using Microsoft.Management.Deployment; using Projection::DevHome.SetupFlow.ElevatedComponent; @@ -55,6 +56,8 @@ public bool DependsOnDevDriveToBeInstalled get; } + public ISummaryInformationViewModel SummaryScreenInformation { get; } + public string PackageName => _package.Name; #pragma warning disable 67 diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index 934d2f05c..5be21c91d 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -101,6 +101,12 @@ public static class StringResourceKey public static readonly string CloneRepoErrorForActionCenter = nameof(CloneRepoErrorForActionCenter); public static readonly string CloneRepoRestart = nameof(CloneRepoRestart); + // Repository Next Steps messages + public static readonly string CloneRepoNextStepsView = nameof(CloneRepoNextStepsView); + public static readonly string CloneRepoNextStepsRun = nameof(CloneRepoNextStepsRun); + public static readonly string CloneRepoNextStepsFileFound = nameof(CloneRepoNextStepsFileFound); + public static readonly string CloneRepoNextStepsDescription = nameof(CloneRepoNextStepsDescription); + // Configure task loading screen messages public static readonly string ApplyingConfigurationMessage = nameof(ApplyingConfigurationMessage); public static readonly string ConfigureTaskCreating = nameof(ConfigureTaskCreating); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 507e334d1..5191c6ae7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -1484,7 +1484,7 @@ The entire configuration is still pending Text for the pending status of the configuration operation that happened on a remote machine. This is for the entire configuration file we're applying - + This part of the configuration is still pending Text for the pending status of the configuration operation that happened on a remote machine. This is for a single part of the configuration we're applying @@ -1542,7 +1542,7 @@ There was an issue applying part of the configuration using DSC resource: '{0}'. Error: {1} - Locked={"0", "{1}"} Text for the ongoing progress of applying a configuration unit in the configuration file on to the remote machine. {0} is the name of the DSC resource we're using for this part of the configuration file, while {1} is the message from the other app. + Locked={"{0}", "{1}"} Text for the ongoing progress of applying a configuration unit in the configuration file on to the remote machine. {0} is the name of the DSC resource we're using for this part of the configuration file, while {1} is the message from the other app. There was an issue applying part of the configuration using DSC resource: '{0}'. Check the extension's logs @@ -1608,4 +1608,20 @@ Loading your environments Test that tells the user that we're still loading their environments. These environments can be virtual or remote machines + + Detected '{0}' while cloning '{1}' + Locked={"{0}", "{1}"} Description letting users know what configuration was found and in what repo the file was found in. {0} is the file name with extension. {1} is the repo name including the owner. + + + Configuration file detected + Text to notify the user a configuration file was found in a repo. + + + Run file + Button content for running a configuration file. + + + View file + Button content for viewing a file. + \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/RepoConfigTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/RepoConfigTaskGroup.cs index feb9c619f..a64ac5c9b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/RepoConfigTaskGroup.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/RepoConfigTaskGroup.cs @@ -69,11 +69,11 @@ public void SaveSetupTaskInformation(List cloningInformation CloneRepoTask task; if (cloningInformation.OwningAccount == null) { - task = new CloneRepoTask(cloningInformation.RepositoryProvider, new DirectoryInfo(cloningInformation.ClonePath), cloningInformation.RepositoryToClone, _stringResource, cloningInformation.RepositoryProviderDisplayName, _activityId); + task = new CloneRepoTask(cloningInformation.RepositoryProvider, new DirectoryInfo(cloningInformation.ClonePath), cloningInformation.RepositoryToClone, _stringResource, cloningInformation.RepositoryProviderDisplayName, _activityId, _host); } else { - task = new CloneRepoTask(cloningInformation.RepositoryProvider, new DirectoryInfo(cloningInformation.ClonePath), cloningInformation.RepositoryToClone, cloningInformation.OwningAccount, _stringResource, cloningInformation.RepositoryProviderDisplayName, _activityId); + task = new CloneRepoTask(cloningInformation.RepositoryProvider, new DirectoryInfo(cloningInformation.ClonePath), cloningInformation.RepositoryToClone, cloningInformation.OwningAccount, _stringResource, cloningInformation.RepositoryProviderDisplayName, _activityId, _host); } if (cloningInformation.CloneToDevDrive) diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/CloneRepoSummaryInformationFileViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/CloneRepoSummaryInformationFileViewModel.cs new file mode 100644 index 000000000..cf434b518 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/CloneRepoSummaryInformationFileViewModel.cs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using CommunityToolkit.Mvvm.Input; +using DevHome.Common.TelemetryEvents.SetupFlow.SummaryPage; +using DevHome.SetupFlow.Services; +using DevHome.Telemetry; +using Serilog; + +namespace DevHome.SetupFlow.ViewModels; + +public partial class CloneRepoSummaryInformationViewModel : ISummaryInformationViewModel +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CloneRepoSummaryInformationViewModel)); + + private const string _eventName = "CloneRepo_NextSteps_Event"; + + private const string _runOperation = "run"; + + private const string _viewOperation = "view"; + + private readonly Guid _relatedActivityId; + + private readonly ISetupFlowStringResource _stringResource; + + public string FileName => Path.GetFileName(FilePathAndName) ?? string.Empty; + + public bool HasContent => !string.IsNullOrEmpty(FilePathAndName) + && !string.IsNullOrEmpty(RepoName) + && !string.IsNullOrEmpty(OwningAccount); + + public string FilePathAndName { get; set; } = string.Empty; + + public string RepoName { get; set; } = string.Empty; + + public string OwningAccount { get; set; } = string.Empty; + + // Using the resource file for properties like .Text and .Content does not work in this case because + // the UserControl is inside a DataTemplate and does not have access to the string resource file. + // any x:Uid used is blank in the view. + // Set the strings here. + public string FileFoundMessage { get; } = string.Empty; + + public string FileDescription + { + get + { + var repoPath = Path.Join(OwningAccount, RepoName); + return _stringResource.GetLocalized(StringResourceKey.CloneRepoNextStepsDescription, FileName, repoPath); + } + } + + public string ViewFileMessage { get; } = string.Empty; + + public string RunFileMessage { get; } = string.Empty; + + public CloneRepoSummaryInformationViewModel(SetupFlowOrchestrator setupFlowOrchestrator, ISetupFlowStringResource stringResource) + { + _relatedActivityId = setupFlowOrchestrator.ActivityId; + _stringResource = stringResource; + FileFoundMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoNextStepsFileFound); + ViewFileMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoNextStepsView); + RunFileMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoNextStepsRun); + } + + [RelayCommand] + public void OpenFileInExplorer() + { + TelemetryFactory.Get().Log( + _eventName, + LogLevel.Critical, + new CloneRepoNextStepEvent(_viewOperation, RepoName), + _relatedActivityId); + + if (FilePathAndName is null) + { + SendTelemetryAndLogError(_viewOperation, new ArgumentNullException(nameof(FilePathAndName))); + return; + } + + if (string.IsNullOrEmpty(FilePathAndName)) + { + SendTelemetryAndLogError(_viewOperation, new ArgumentNullException(nameof(FilePathAndName))); + return; + } + + var processStartInfo = new ProcessStartInfo(); + processStartInfo.UseShellExecute = true; + + try + { + // Not catching PathTooLongException. If the file was in a location that had a too long path, + // the repo, when cloning, would run into a PathTooLongException and repo would not be cloned. + processStartInfo.FileName = Path.GetDirectoryName(FilePathAndName); + } + catch (ArgumentException ex) + { + SendTelemetryAndLogError(_viewOperation, ex); + return; + } + + StartProcess(processStartInfo, _viewOperation); + } + + [RelayCommand] + public void RunInAdminCommandPrompt() + { + TelemetryFactory.Get().Log( + _eventName, + LogLevel.Critical, + new CloneRepoNextStepEvent(_runOperation, RepoName), + _relatedActivityId); + + if (FileName is null) + { + SendTelemetryAndLogError(_runOperation, new ArgumentNullException(nameof(FileName))); + return; + } + + if (FilePathAndName is null) + { + SendTelemetryAndLogError(_runOperation, new ArgumentNullException(nameof(FileName))); + return; + } + + var processStartInfo = new ProcessStartInfo(); + processStartInfo.UseShellExecute = true; + processStartInfo.FileName = "winget"; + processStartInfo.ArgumentList.Add("configure"); + processStartInfo.ArgumentList.Add(FilePathAndName); + processStartInfo.Verb = "RunAs"; + + StartProcess(processStartInfo, _runOperation); + } + + private void StartProcess(ProcessStartInfo processStartInfo, string operation) + { + try + { + Process.Start(processStartInfo); + } + catch (Win32Exception win32Exception) + { + // Usually because the UAC prompt was declined. + SendTelemetryAndLogError(operation, win32Exception); + } + catch (ObjectDisposedException objectDisposedException) + { + SendTelemetryAndLogError(operation, objectDisposedException); + } + catch (InvalidOperationException invalidOperationException) + { + SendTelemetryAndLogError(operation, invalidOperationException); + } + catch (Exception e) + { + SendTelemetryAndLogError(operation, e); + } + } + + private void SendTelemetryAndLogError(string operation, Exception e) + { + TelemetryFactory.Get().LogError( + _eventName, + LogLevel.Critical, + new CloneRepoNextStepError(operation, e.ToString(), RepoName), + _relatedActivityId); + + _log.Error(e, string.Empty); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationSummaryInformationViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationSummaryInformationViewModel.cs new file mode 100644 index 000000000..45cbde1c2 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationSummaryInformationViewModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.SetupFlow.ViewModels; + +public partial class ConfigurationSummaryInformationViewModel : ObservableRecipient, ISummaryInformationViewModel +{ + public bool HasContent => false; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/CreateDevDriveSummaryInformationViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/CreateDevDriveSummaryInformationViewModel.cs new file mode 100644 index 000000000..4b8aeba0d --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/CreateDevDriveSummaryInformationViewModel.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.SetupFlow.ViewModels; + +public partial class CreateDevDriveSummaryInformationViewModel : ObservableRecipient, ISummaryInformationViewModel +{ + public bool HasContent => false; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/EmptySummaryInformationViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/EmptySummaryInformationViewModel.cs new file mode 100644 index 000000000..35e58f94f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/EmptySummaryInformationViewModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.SetupFlow.ViewModels; + +public partial class EmptySummaryInformationViewModel : ObservableRecipient, ISummaryInformationViewModel +{ + public bool HasContent => false; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ISummaryInformationViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ISummaryInformationViewModel.cs new file mode 100644 index 000000000..a60bcbce1 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ISummaryInformationViewModel.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace DevHome.SetupFlow.ViewModels; + +public interface ISummaryInformationViewModel +{ + /// + /// Gets a value indicating whether this object has enough data to be used + /// in the next steps portion of the summary screen. + /// + public bool HasContent { get; } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/InstallPackageSummaryInformationViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/InstallPackageSummaryInformationViewModel.cs new file mode 100644 index 000000000..c294db1df --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/InstallPackageSummaryInformationViewModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.ComponentModel; + +namespace DevHome.SetupFlow.ViewModels; + +public partial class InstallPackageSummaryInformationViewModel : ObservableRecipient, ISummaryInformationViewModel +{ + public bool HasContent => false; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingMessageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingMessageViewModel.cs index 1818dcc6f..3e6c248c3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingMessageViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingMessageViewModel.cs @@ -15,7 +15,8 @@ public partial class LoadingMessageViewModel : ObservableObject /// /// Gets the message to display in the loading screen. /// - public string MessageToShow { get; } + [ObservableProperty] + private string _messageToShow; /// /// If the progress ring should be shown. Only show a progress ring when the task is running. @@ -44,9 +45,4 @@ public void TextTrimmed() { IsRepoNameTrimmed = true; } - - public LoadingMessageViewModel(string messageToShow) - { - MessageToShow = messageToShow; - } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs index 06b4684d1..174713750 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/LoadingViewModel.cs @@ -42,10 +42,6 @@ public partial class LoadingViewModel : SetupPageViewModelBase private static readonly BitmapImage LightError = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/LightError.png")); private static readonly BitmapImage LightSuccess = new(new Uri("ms-appx:///DevHome.SetupFlow/Assets/LightSuccess.png")); -#pragma warning disable SA1310 // Field names should not contain underscore - private const int NUMBER_OF_PARALLEL_RUNNING_TASKS = 5; -#pragma warning restore SA1310 // Field names should not contain underscore - #pragma warning disable SA1310 // Field names should not contain underscore private const int MAX_RETRIES = 1; #pragma warning restore SA1310 // Field names should not contain underscore @@ -81,6 +77,9 @@ public partial class LoadingViewModel : SetupPageViewModelBase [ObservableProperty] private ObservableCollection _messages; + [ObservableProperty] + private ObservableCollection _summaryInformation; + /// /// List of all messages that shows up in the "action center" of the loading screen. /// @@ -182,7 +181,8 @@ public void AddMessage(string message, MessageSeverityKind severityKind = Messag { Application.Current.GetService().DispatcherQueue.TryEnqueue(() => { - var messageToDisplay = new LoadingMessageViewModel(message); + var messageToDisplay = _host.GetService(); + messageToDisplay.MessageToShow = message; messageToDisplay.ShouldShowStatusSymbolIcon = false; messageToDisplay.ShouldShowProgressRing = false; @@ -250,6 +250,7 @@ public LoadingViewModel( ActionCenterItems = new(); Messages = new(); _activityId = orchestrator.ActivityId; + _summaryInformation = new ObservableCollection(); } // Remove all tasks except for the SetupTarget @@ -365,7 +366,8 @@ private void ChangeMessage(TaskInformation information, LoadingMessageViewModel Messages.Insert(Messages.Count - _numberOfExecutingTasks, loadingMessage); // Add the "Execution finished" message - var newLoadingScreenMessage = new LoadingMessageViewModel(stringToReplace); + var newLoadingScreenMessage = _host.GetService(); + newLoadingScreenMessage.MessageToShow = stringToReplace; newLoadingScreenMessage.StatusSymbolIcon = statusSymbolIcon; newLoadingScreenMessage.ShouldShowProgressRing = false; newLoadingScreenMessage.ShouldShowStatusSymbolIcon = true; @@ -412,19 +414,14 @@ await Task.Run(async () => } } - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = NUMBER_OF_PARALLEL_RUNNING_TASKS, - }; - // Run all tasks that don't need dev drive installed. - await Parallel.ForEachAsync(tasksToRunFirst, options, async (taskInformation, token) => + await Parallel.ForEachAsync(tasksToRunFirst, async (taskInformation, token) => { await StartTaskAndReportResult(window, taskInformation); }); // Run all the tasks that need dev drive installed. - await Parallel.ForEachAsync(tasksToRunSecond, options, async (taskInformation, token) => + await Parallel.ForEachAsync(tasksToRunSecond, async (taskInformation, token) => { await StartTaskAndReportResult(window, taskInformation); }); @@ -469,7 +466,8 @@ private async Task StartTaskAndReportResult(WinUIEx.WindowEx window, TaskInforma // Start the task and wait for it to complete. try { - var loadingMessage = new LoadingMessageViewModel(taskInformation.MessageToShow); + var loadingMessage = _host.GetService(); + loadingMessage.MessageToShow = taskInformation.MessageToShow; window.DispatcherQueue.TryEnqueue(() => { TasksStarted++; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs index 2eefd960e..d03e2d699 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs @@ -12,13 +12,16 @@ using DevHome.Common.Extensions; using DevHome.Common.Services; using DevHome.Common.TelemetryEvents.SetupFlow; +using DevHome.Common.TelemetryEvents.SetupFlow.SummaryPage; using DevHome.Contracts.Services; using DevHome.SetupFlow.Models; using DevHome.SetupFlow.Services; using DevHome.SetupFlow.TaskGroups; +using DevHome.SetupFlow.Views; using DevHome.Telemetry; using Microsoft.Extensions.Hosting; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Imaging; using Serilog; using Windows.System; @@ -39,6 +42,14 @@ public partial class SummaryViewModel : SetupPageViewModelBase private readonly PackageProvider _packageProvider; private readonly IAppManagementInitializer _appManagementInitializer; + private readonly List _cloneRepoNextSteps; + + // Holds all the UI to display for "Next Steps". + public List NextSteps => _cloneRepoNextSteps; + + [ObservableProperty] + private ObservableCollection _summaryInformation; + [ObservableProperty] private List _failedTasks = new(); @@ -187,6 +198,7 @@ public SummaryViewModel( _configurationUnitResults = new(GetConfigurationUnitResults); _showRestartNeeded = Visibility.Collapsed; _appManagementInitializer = appManagementInitializer; + _cloneRepoNextSteps = new(); IsNavigationBarVisible = true; IsStepPage = false; @@ -205,6 +217,31 @@ protected async override Task OnFirstNavigateToAsync() } } + // Collect all next steps. + var taskGroups = Orchestrator.TaskGroups; + foreach (var taskGroup in taskGroups) + { + var setupTasks = taskGroup.SetupTasks; + foreach (var setupTask in setupTasks) + { + if (setupTask.SummaryScreenInformation is not null && + setupTask.SummaryScreenInformation.HasContent) + { + switch (setupTask) + { + case CloneRepoTask: + var configResult = new CloneRepoSummaryInformationView(); + configResult.DataContext = setupTask.SummaryScreenInformation; + _cloneRepoNextSteps.Add(configResult); + break; + } + } + } + } + + // Send telemetry about the number of next steps tasks found broken down by their type. + ReportSummaryTaskCounts(_cloneRepoNextSteps.Count); + BitmapImage statusSymbol; if (_host.GetService().Theme == ElementTheme.Dark) { @@ -233,6 +270,14 @@ protected async override Task OnFirstNavigateToAsync() await ReloadCatalogsAsync(); } + /// + /// Send telemetry about all next steps. + /// + private void ReportSummaryTaskCounts(int cloneRepoNextStepsCount) + { + TelemetryFactory.Get().Log("Summary_NextSteps_Event", LogLevel.Critical, new CloneRepoNextStepsEvent(cloneRepoNextStepsCount), Orchestrator.ActivityId); + } + private async Task ReloadCatalogsAsync() { // After installing packages, reconnect to catalogs to diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/CloneRepoSummaryInformationView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/CloneRepoSummaryInformationView.xaml new file mode 100644 index 000000000..f892ac970 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/CloneRepoSummaryInformationView.xaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/CloneRepoSummaryInformationView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/CloneRepoSummaryInformationView.xaml.cs new file mode 100644 index 000000000..03a69cd71 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/CloneRepoSummaryInformationView.xaml.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using DevHome.SetupFlow.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +namespace DevHome.SetupFlow.Views; + +public sealed partial class CloneRepoSummaryInformationView : UserControl +{ + public CloneRepoSummaryInformationViewModel ViewModel => (CloneRepoSummaryInformationViewModel)this.DataContext; + + public CloneRepoSummaryInformationView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml index 5681f705c..a245c9c92 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml @@ -27,7 +27,7 @@ - + + 0, 1, 0, 2 @@ -119,7 +120,7 @@ - + @@ -197,7 +198,8 @@ - + + @@ -205,7 +207,7 @@ HorizontalAlignment="Stretch" Padding="0,15,0,30" BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" - BorderThickness="{ThemeResource TopNavigationViewContentGridBorderThickness}"> + BorderThickness="{ThemeResource TopLeftQuadrantThickness}"> @@ -259,7 +261,7 @@ HorizontalAlignment="Stretch" Padding="0,15,0,30" BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" - BorderThickness="{ThemeResource TopNavigationViewContentGridBorderThickness}"> + BorderThickness="{ThemeResource TopLeftQuadrantThickness}"> @@ -267,6 +269,7 @@ + + Grid.Column="1" + Visibility="{x:Bind ViewModel.RepositoriesCloned, Converter={StaticResource CollectionVisibilityConverter}}"> @@ -314,7 +317,18 @@ Margin="0, 50, 0, 0"/> - + + + + + + + + + + + + diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml.cs index eff984525..c89feb8d8 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml.cs @@ -9,7 +9,7 @@ namespace DevHome.SetupFlow.Views; public sealed partial class SummaryView : UserControl -{ +{ public SummaryView() { this.InitializeComponent();