diff --git a/.github/policies/moderatorTriggers.yml b/.github/policies/moderatorTriggers.yml index 9b5608c01..4b92473c6 100644 --- a/.github/policies/moderatorTriggers.yml +++ b/.github/policies/moderatorTriggers.yml @@ -90,8 +90,9 @@ configuration: reply: >- Hi @${issueAuthor}. It would be helpful for us if you could share your Dev Home logs. These logs can be found at - `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHome_8wekyb3d8bbwe\TempState` and - `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHomeGitHubExtension_8wekyb3d8bbwe\TempState`. + `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHome_8wekyb3d8bbwe\TempState`, + `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHomeGitHubExtension_8wekyb3d8bbwe\TempState`, and + `%LOCALAPPDATA%\Packages\Microsoft.Windows.DevHomeAzureExtension_8wekyb3d8bbwe\TempState`. You can share these folders via a OneDrive link or zip them and attach them to a comment here. If you share this way, you may want to look through the logs in case there are any details included that you would like to remove (for example, private diff --git a/.github/workflows/DevHome-CI.yml b/.github/workflows/DevHome-CI.yml index 8290067b5..f633cff6a 100644 --- a/.github/workflows/DevHome-CI.yml +++ b/.github/workflows/DevHome-CI.yml @@ -72,7 +72,7 @@ jobs: - name: Compress_DevSetupAgent_x86 if: ${{ matrix.platform != 'arm64' }} shell: pwsh - run: Compress-Archive -Force -Path HyperVExtension\src\DevSetupAgent\bin\x86\${{ matrix.configuration }}\net8.0-windows10.0.22621.0\win10-x86\* -DestinationPath "HyperVExtension\src\DevSetupAgent\bin\x86\${{ matrix.configuration }}\DevSetupAgent_x86.zip" + run: Compress-Archive -Force -Path HyperVExtension\src\DevSetupAgent\bin\x86\${{ matrix.configuration }}\net8.0-windows10.0.22621.0\win-x86\* -DestinationPath "HyperVExtension\src\DevSetupAgent\bin\x86\${{ matrix.configuration }}\DevSetupAgent_x86.zip" - name: Build_DevSetupAgent_arm64 if: ${{ matrix.platform == 'arm64' }} @@ -81,7 +81,7 @@ jobs: - name: Compress_DevSetupAgent_arm64 if: ${{ matrix.platform == 'arm64' }} shell: pwsh - run: Compress-Archive -Force -Path HyperVExtension\src\DevSetupAgent\bin\arm64\${{ matrix.configuration }}\net8.0-windows10.0.22621.0\win10-arm64\* -DestinationPath "HyperVExtension\src\DevSetupAgent\bin\arm64\${{ matrix.configuration }}\DevSetupAgent_arm64.zip" + run: Compress-Archive -Force -Path HyperVExtension\src\DevSetupAgent\bin\arm64\${{ matrix.configuration }}\net8.0-windows10.0.22621.0\win-arm64\* -DestinationPath "HyperVExtension\src\DevSetupAgent\bin\arm64\${{ matrix.configuration }}\DevSetupAgent_arm64.zip" - name: Build_DevHome run: cmd /c "$env:VSDevCmd" "&" msbuild /p:Configuration=${{ matrix.configuration }},Platform=${{ matrix.platform }} DevHome.sln diff --git a/Directory.Build.props b/Directory.Build.props index 2d989711e..4f951f1b9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,6 +12,7 @@ <EnableNETAnalyzers>true</EnableNETAnalyzers> <AnalysisMode>Recommended</AnalysisMode> <PlatformTarget>$(Platform)</PlatformTarget> + <ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles> </PropertyGroup> <!-- @@ -38,12 +39,5 @@ <Compile Condition="$(MSBuildProjectName) == 'DevHomeStub'" Include="$(MSBuildThisFileDirectory)\codeAnalysis\StubSuppressions.cs" Link="GlobalSuppressions.cs" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)\codeAnalysis\StyleCop.json" Link="StyleCop.json" /> </ItemGroup> - <!-- Needed for reverting back to pre-.NET 8 method of Host using the RID graph to determine assets --> - <!-- https://learn.microsoft.com/en-us/dotnet/core/compatibility/deployment/8.0/rid-asset-list --> - <PropertyGroup> - <UseRidGraph>true</UseRidGraph> - </PropertyGroup> - <ItemGroup> - <RuntimeHostConfigurationOption Include="System.Runtime.Loader.UseRidGraph" Value="true" /> - </ItemGroup> + </Project> \ No newline at end of file diff --git a/HyperVExtension/BuildDevSetupAgentHelper.ps1 b/HyperVExtension/BuildDevSetupAgentHelper.ps1 index 4d64e35c6..3be5d8836 100644 --- a/HyperVExtension/BuildDevSetupAgentHelper.ps1 +++ b/HyperVExtension/BuildDevSetupAgentHelper.ps1 @@ -93,7 +93,7 @@ Try { & $msbuildPath $msbuildArgs # SDK version and .NEt version needs to stay in sync with ToolingVersion.props, DevSetupEngineIdl.vcxproj, and DevHome-CL.yaml - $binariesOutputPath = (Join-Path $env:Build_RootDirectory "HyperVExtension\src\DevSetupAgent\bin\$Platform\$Configuration\net8.0-windows10.0.22621.0\win10-$Platform\*") + $binariesOutputPath = (Join-Path $env:Build_RootDirectory "HyperVExtension\src\DevSetupAgent\bin\$Platform\$Configuration\net8.0-windows10.0.22621.0\win-$Platform\*") $zipOutputPath = (Join-Path $env:Build_RootDirectory "HyperVExtension\src\DevSetupAgent\bin\$Platform\$Configuration\DevSetupAgent_$Platform.zip") Compress-Archive -Force -Path $binariesOutputPath $zipOutputPath diff --git a/HyperVExtension/src/DevSetupAgent/DevAgentService.cs b/HyperVExtension/src/DevSetupAgent/DevAgentService.cs index f0b5c958c..9b5b89a18 100644 --- a/HyperVExtension/src/DevSetupAgent/DevAgentService.cs +++ b/HyperVExtension/src/DevSetupAgent/DevAgentService.cs @@ -41,13 +41,13 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken) } catch (Exception ex) { - _log.Error($"Exception in DevAgentService.", ex); + _log.Error(ex, $"Exception in DevAgentService."); } } } catch (Exception ex) { - _log.Error($"Failed to run DevSetupAgent.", ex); + _log.Error(ex, $"Failed to run DevSetupAgent."); throw; } finally diff --git a/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj b/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj index ec94f1721..b0ce1b650 100644 --- a/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj +++ b/HyperVExtension/src/DevSetupAgent/DevSetupAgent.csproj @@ -6,8 +6,8 @@ <UserSecretsId>dotnet-DevSetupAgent-674f51cd-70a6-4b78-8376-66efbf84c412</UserSecretsId> <BuildRing Condition="'$(BuildRing)'==''">Dev</BuildRing> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> - <PublishProfile Condition="'$(BuildingInsideVisualStudio)' != 'True'">Properties\PublishProfiles\win10-$(Platform).pubxml</PublishProfile> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> + <PublishProfile Condition="'$(BuildingInsideVisualStudio)' != 'True'">Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile> <SelfContained>true</SelfContained> </PropertyGroup> diff --git a/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs b/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs index 518dc93ed..11e8f8a07 100644 --- a/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs +++ b/HyperVExtension/src/DevSetupAgent/HostRegistryChannel.cs @@ -113,7 +113,7 @@ await Task.Run( } catch (Exception ex) { - _log.Error($"Could not write host message. Response ID: {responseMessage.ResponseId}", ex); + _log.Error(ex, $"Could not write host message. Response ID: {responseMessage.ResponseId}"); } }, stoppingToken); @@ -130,7 +130,7 @@ await Task.Run( } catch (Exception ex) { - _log.Error($"Could not delete host message. Response ID: {responseId}", ex); + _log.Error(ex, $"Could not delete host message. Response ID: {responseId}"); } }, stoppingToken); @@ -207,7 +207,7 @@ private RequestMessage TryReadMessage() } catch (Exception ex) { - _log.Error($"Could not read host message {valueName}", ex); + _log.Error(ex, $"Could not read host message {valueName}"); } MessageHelper.DeleteAllMessages(_registryHiveKey, _fromHostRegistryKeyPath, s[0]); @@ -218,7 +218,7 @@ private RequestMessage TryReadMessage() } catch (Exception ex) { - _log.Error("Could not read host message.", ex); + _log.Error(ex, "Could not read host message."); } return requestMessage; diff --git a/HyperVExtension/src/DevSetupAgent/Logging.cs b/HyperVExtension/src/DevSetupAgent/Logging.cs new file mode 100644 index 000000000..920111d91 --- /dev/null +++ b/HyperVExtension/src/DevSetupAgent/Logging.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Windows.Storage; + +namespace HyperVExtension.DevSetupAgent; + +public class Logging +{ + public static readonly string LogExtension = ".dhlog"; + + public static readonly string LogFolderName = "Logs"; + + public static readonly string AppName = "DevSetupAgent"; + + public static readonly string DefaultLogFileName = "hyperv_setup"; + + private static readonly Lazy<string> _logFolderRoot = new(() => Path.Combine(Path.GetTempPath(), AppName, LogFolderName)); + + public static readonly string LogFolderRoot = _logFolderRoot.Value; +} diff --git a/HyperVExtension/src/DevSetupAgent/NativeMethods.txt b/HyperVExtension/src/DevSetupAgent/NativeMethods.txt index 13b6a24b0..82f030d24 100644 --- a/HyperVExtension/src/DevSetupAgent/NativeMethods.txt +++ b/HyperVExtension/src/DevSetupAgent/NativeMethods.txt @@ -6,8 +6,14 @@ CLSCTX WIN32_ERROR S_OK E_FAIL +E_UNEXPECTED LsaEnumerateLogonSessions LsaGetLogonSessionData Windows.Win32.Security.Authentication.Identity.LsaFreeReturnBuffer SECURITY_LOGON_TYPE STATUS_SUCCESS +MakeAbsoluteSD +ConvertStringSecurityDescriptorToSecurityDescriptor +LocalAlloc +LocalFree +SDDL_REVISION_1 diff --git a/HyperVExtension/src/DevSetupAgent/Program.cs b/HyperVExtension/src/DevSetupAgent/Program.cs index d8dbf882e..dbc372a24 100644 --- a/HyperVExtension/src/DevSetupAgent/Program.cs +++ b/HyperVExtension/src/DevSetupAgent/Program.cs @@ -1,34 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.ComponentModel; using System.Runtime.InteropServices; using HyperVExtension.DevSetupAgent; +using HyperVExtension.HostGuestCommunication; using Serilog; using Windows.Win32; +using Windows.Win32.Foundation; using Windows.Win32.Security; using Windows.Win32.System.Com; -unsafe -{ - // TODO: Set real security descriptor to allow access from System+Admns+Interactive Users - var hr = PInvoke.CoInitializeSecurity( - new(null), - -1, - null, - null, - RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, - RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IDENTIFY, - null, - EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); - - if (hr < 0) - { - Marshal.ThrowExceptionForHR(hr); - } -} - // Set up Logging -Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(HyperVExtension.DevSetupEngine.Logging.LogFolderRoot, "HyperV")); +Environment.SetEnvironmentVariable("DEVHOME_LOGS_ROOT", Path.Join(Logging.LogFolderRoot, "HyperV")); var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings_hypervsetupagent.json") .Build(); @@ -36,6 +20,90 @@ .ReadFrom.Configuration(configuration) .CreateLogger(); +unsafe +{ + PSECURITY_DESCRIPTOR absolutSd = new(null); + PSID ownerSid = new(null); + PSID groupSid = new(null); + ACL* dacl = default; + ACL* sacl = default; + + try + { + // O:PSG:BU Owner Principal Self, Group Built-in Users + // (A;;0x3;;;SY) Allow Local System + // (A;;0x3;;;IU) Allow Interactive User + var accessPermission = "O:PSG:BUD:(A;;0x3;;;SY)(A;;0x3;;;IU)"; + uint securityDescriptorSize; + PInvoke.ConvertStringSecurityDescriptorToSecurityDescriptor(accessPermission, PInvoke.SDDL_REVISION_1, out var securityDescriptor, &securityDescriptorSize); + + uint absoluteSdSize = default; + uint daclSize = default; + uint saclSize = default; + uint ownerSize = default; + uint groupSize = default; + + if (PInvoke.MakeAbsoluteSD(securityDescriptor, absolutSd, ref absoluteSdSize, null, ref daclSize, null, ref saclSize, ownerSid, ref ownerSize, groupSid, ref groupSize)) + { + throw new HResultException(HRESULT.E_UNEXPECTED); + } + + var error = Marshal.GetLastWin32Error(); + if (error != (int)WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) + { + throw new Win32Exception(error); + } + + absolutSd = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, absoluteSdSize)); + dacl = (ACL*)PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, daclSize); + sacl = (ACL*)PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, saclSize); + ownerSid = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, ownerSize)); + groupSid = new(PInvoke.LocalAlloc(Windows.Win32.System.Memory.LOCAL_ALLOC_FLAGS.LPTR, groupSize)); + + if (!PInvoke.MakeAbsoluteSD(securityDescriptor, absolutSd, ref absoluteSdSize, dacl, ref daclSize, sacl, ref saclSize, ownerSid, ref ownerSize, groupSid, ref groupSize)) + { + throw new HResultException(Marshal.GetLastWin32Error()); + } + + var hr = PInvoke.CoInitializeSecurity( + absolutSd, + -1, + null, + null, + RPC_C_AUTHN_LEVEL.RPC_C_AUTHN_LEVEL_DEFAULT, + RPC_C_IMP_LEVEL.RPC_C_IMP_LEVEL_IDENTIFY, + null, + EOLE_AUTHENTICATION_CAPABILITIES.EOAC_NONE); + + if (hr < 0) + { + Marshal.ThrowExceptionForHR(hr); + } + } + finally + { + if (sacl != default) + { + PInvoke.LocalFree((HLOCAL)sacl); + } + + if (dacl != default) + { + PInvoke.LocalFree((HLOCAL)dacl); + } + + if (groupSid != default) + { + PInvoke.LocalFree((HLOCAL)groupSid.Value); + } + + if (ownerSid != default) + { + PInvoke.LocalFree((HLOCAL)ownerSid.Value); + } + } +} + var host = Host.CreateDefaultBuilder(args) .UseWindowsService(options => { diff --git a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-arm64.pubxml b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-arm64.pubxml similarity index 90% rename from HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-arm64.pubxml rename to HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-arm64.pubxml index 08079c293..ced5ea324 100644 --- a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-arm64.pubxml +++ b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-arm64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>arm64</Platform> - <RuntimeIdentifier>win10-arm64</RuntimeIdentifier> + <RuntimeIdentifier>win-arm64</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x64.pubxml b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x64.pubxml similarity index 91% rename from HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x64.pubxml rename to HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x64.pubxml index 94861ecd4..e4ca421fa 100644 --- a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x64.pubxml +++ b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>x64</Platform> - <RuntimeIdentifier>win10-x64</RuntimeIdentifier> + <RuntimeIdentifier>win-x64</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x86.pubxml b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x86.pubxml similarity index 91% rename from HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x86.pubxml rename to HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x86.pubxml index 3a63ea8fb..69092cd4a 100644 --- a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-x86.pubxml +++ b/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win-x86.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>x86</Platform> - <RuntimeIdentifier>win10-x86</RuntimeIdentifier> + <RuntimeIdentifier>win-x86</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs b/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs index 019407c88..023f46ca2 100644 --- a/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs +++ b/HyperVExtension/src/DevSetupAgent/RegistryWatcher.cs @@ -76,14 +76,14 @@ public void Start() } catch (Exception ex) { - _log.Error("RegistryChanged delegate failed.", ex); + _log.Error(ex, "RegistryChanged delegate failed."); } } } } catch (Exception ex) { - _log.Error("Registry Watcher thread failed.", ex); + _log.Error(ex, "Registry Watcher thread failed."); } }); _log.Information("Registry Watcher thread started."); diff --git a/HyperVExtension/src/DevSetupAgent/RequestManager.cs b/HyperVExtension/src/DevSetupAgent/RequestManager.cs index 9c799d77f..07cde9cc4 100644 --- a/HyperVExtension/src/DevSetupAgent/RequestManager.cs +++ b/HyperVExtension/src/DevSetupAgent/RequestManager.cs @@ -102,7 +102,7 @@ private void ProcessRequestQueue(CancellationToken stoppingToken) } catch (Exception ex) { - _log.Error($"Failed to execute request.", ex); + _log.Error(ex, $"Failed to execute request."); } } } diff --git a/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs b/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs index fabc9a205..725f3dedf 100644 --- a/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs +++ b/HyperVExtension/src/DevSetupAgent/Requests/ErrorRequest.cs @@ -9,10 +9,11 @@ namespace HyperVExtension.DevSetupAgent; /// </summary> internal class ErrorRequest : IHostRequest { - public ErrorRequest(IRequestMessage requestMessage) + public ErrorRequest(IRequestMessage requestMessage, Exception? ex = null) { Timestamp = DateTime.UtcNow; RequestId = requestMessage.RequestId!; + Error = ex; } public virtual uint Version { get; set; } = 1; @@ -27,6 +28,8 @@ public ErrorRequest(IRequestMessage requestMessage) public virtual IHostResponse Execute(ProgressHandler progressHandler, CancellationToken stoppingToken) { - return new ErrorResponse(RequestId); + return new ErrorResponse(RequestId, Error); } + + private Exception? Error { get; } } diff --git a/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs b/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs index 706763444..bd15df891 100644 --- a/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs +++ b/HyperVExtension/src/DevSetupAgent/Requests/RequestFactory.cs @@ -42,7 +42,6 @@ public IHostRequest CreateRequest(IRequestContext requestContext) { if (_requestFactories.TryGetValue(requestType, out var createRequest)) { - // TODO: Try/catch error. requestContext.JsonData = requestJson!; return createRequest(requestContext); } @@ -65,7 +64,7 @@ public IHostRequest CreateRequest(IRequestContext requestContext) { var messageId = requestContext.RequestMessage.RequestId ?? "<unknown>"; var requestData = requestContext.RequestMessage.RequestData ?? "<unknown>"; - _log.Error($"Error processing message. Message ID: {messageId}. Request data: {requestData}", ex); + _log.Error(ex, $"Error processing message. Message ID: {messageId}. Request data: {requestData}"); return new ErrorRequest(requestContext.RequestMessage); } } diff --git a/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs b/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs index 4428ebf76..da53faf19 100644 --- a/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs +++ b/HyperVExtension/src/DevSetupAgent/Responses/ErrorResponse.cs @@ -9,11 +9,20 @@ namespace HyperVExtension.DevSetupAgent; /// </summary> internal sealed class ErrorResponse : ResponseBase { - public ErrorResponse(string requestId) + public ErrorResponse(string requestId, Exception? error) : base(requestId) { - Status = Windows.Win32.Foundation.HRESULT.E_FAIL; - ErrorDescription = "Missing Request data."; + if (error != null) + { + ErrorDescription = error.Message; + Status = (uint)error.HResult; + } + else + { + ErrorDescription = "Missing Request data."; + Status = Windows.Win32.Foundation.HRESULT.E_FAIL; + } + GenerateJsonData(); } } diff --git a/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs b/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs index c0e0331f1..e57dfd4a9 100644 --- a/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs +++ b/HyperVExtension/src/DevSetupAgent/Responses/ErrorUnsupportedRequestResponse.cs @@ -13,7 +13,7 @@ public ErrorUnsupportedRequestResponse(string requestId, string requestType) : base(requestId, requestType) { Status = Windows.Win32.Foundation.HRESULT.E_FAIL; - ErrorDescription = "Missing Request type."; + ErrorDescription = "Unsupported Request type."; GenerateJsonData(); } } diff --git a/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json b/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json index e5932df92..b66927f70 100644 --- a/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json +++ b/HyperVExtension/src/DevSetupAgent/appsettings_hypervsetupagent.json @@ -12,7 +12,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -20,7 +20,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv_setupagent.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs b/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs index c7d7862dd..c8885f2d4 100644 --- a/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs +++ b/HyperVExtension/src/DevSetupEngine/ConfigurationFileHelper.cs @@ -279,22 +279,21 @@ private void LogConfigurationDiagnostics(WinGet.IDiagnosticInformation diagnosti { _log.Information($"WinGet: {diagnosticInformation.Message}"); - var sourceComponent = nameof(WinGet.ConfigurationProcessor); switch (diagnosticInformation.Level) { case WinGet.DiagnosticLevel.Warning: - _log.Warning(sourceComponent, diagnosticInformation.Message); + _log.Warning(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Error: - _log.Error(sourceComponent, diagnosticInformation.Message); + _log.Error(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Critical: - _log.Fatal(sourceComponent, diagnosticInformation.Message); + _log.Fatal(diagnosticInformation.Message); return; case WinGet.DiagnosticLevel.Verbose: case WinGet.DiagnosticLevel.Informational: default: - _log.Information(sourceComponent, diagnosticInformation.Message); + _log.Information(diagnosticInformation.Message); return; } } diff --git a/HyperVExtension/src/DevSetupEngine/DevSetupEngine.csproj b/HyperVExtension/src/DevSetupEngine/DevSetupEngine.csproj index a2bfa10cb..448a52e47 100644 --- a/HyperVExtension/src/DevSetupEngine/DevSetupEngine.csproj +++ b/HyperVExtension/src/DevSetupEngine/DevSetupEngine.csproj @@ -17,8 +17,8 @@ <BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuildRing Condition="'$(BuildRing)'==''">Dev</BuildRing> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> - <PublishProfile Condition="'$(BuildingInsideVisualStudio)' != 'True'">Properties\PublishProfiles\win10-$(Platform).pubxml</PublishProfile> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> + <PublishProfile Condition="'$(BuildingInsideVisualStudio)' != 'True'">Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile> <SelfContained>true</SelfContained> </PropertyGroup> diff --git a/HyperVExtension/src/DevSetupEngine/Logging.cs b/HyperVExtension/src/DevSetupEngine/Logging.cs index e3e5e4043..a2cdefcdf 100644 --- a/HyperVExtension/src/DevSetupEngine/Logging.cs +++ b/HyperVExtension/src/DevSetupEngine/Logging.cs @@ -9,11 +9,13 @@ public class Logging { public static readonly string LogExtension = ".dhlog"; - public static readonly string LogFolderName = "Logs"; + public static readonly string LogFolderName = "Logs"; + + public static readonly string AppName = "DevSetupEngine"; public static readonly string DefaultLogFileName = "hyperv_setup"; - - private static readonly Lazy<string> _logFolderRoot = new(() => Path.Combine(ApplicationData.Current.TemporaryFolder.Path, LogFolderName)); + + private static readonly Lazy<string> _logFolderRoot = new(() => Path.Combine(Path.GetTempPath(), AppName, LogFolderName)); public static readonly string LogFolderRoot = _logFolderRoot.Value; } diff --git a/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs b/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs index 9b149bede..eaca912ed 100644 --- a/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs +++ b/HyperVExtension/src/DevSetupEngine/PackageOperationException.cs @@ -19,6 +19,6 @@ public PackageOperationException(ErrorCode errorCode, string message) { HResult = (int)errorCode; var log = Log.ForContext("SourceContext", nameof(PackageOperationException)); - log.Error(message, this); + log.Error(this, message); } } diff --git a/HyperVExtension/src/DevSetupEngine/Program.cs b/HyperVExtension/src/DevSetupEngine/Program.cs index 6bff3af2d..4c1c269eb 100644 --- a/HyperVExtension/src/DevSetupEngine/Program.cs +++ b/HyperVExtension/src/DevSetupEngine/Program.cs @@ -58,7 +58,7 @@ public static int Main([System.Runtime.InteropServices.WindowsRuntime.ReadOnlyAr } catch (Exception ex) { - Log.Error($"Exception: {ex}", ex); + Log.Error(ex, $"Exception: {ex}"); Log.CloseAndFlush(); return ex.HResult; } diff --git a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-arm64.pubxml b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-arm64.pubxml similarity index 90% rename from HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-arm64.pubxml rename to HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-arm64.pubxml index 08079c293..ced5ea324 100644 --- a/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win10-arm64.pubxml +++ b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-arm64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>arm64</Platform> - <RuntimeIdentifier>win10-arm64</RuntimeIdentifier> + <RuntimeIdentifier>win-arm64</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x64.pubxml b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x64.pubxml similarity index 91% rename from HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x64.pubxml rename to HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x64.pubxml index 94861ecd4..e4ca421fa 100644 --- a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x64.pubxml +++ b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>x64</Platform> - <RuntimeIdentifier>win10-x64</RuntimeIdentifier> + <RuntimeIdentifier>win-x64</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x86.pubxml b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x86.pubxml similarity index 91% rename from HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x86.pubxml rename to HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x86.pubxml index 3a63ea8fb..69092cd4a 100644 --- a/HyperVExtension/src/DevSetupAgent/Properties/PublishProfiles/win10-x86.pubxml +++ b/HyperVExtension/src/DevSetupEngine/Properties/PublishProfiles/win-x86.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>x86</Platform> - <RuntimeIdentifier>win10-x86</RuntimeIdentifier> + <RuntimeIdentifier>win-x86</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json b/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json index 9653f5028..ce815ea0b 100644 --- a/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json +++ b/HyperVExtension/src/DevSetupEngine/appsettings_hypervsetup.json @@ -6,7 +6,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -14,7 +14,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv_setup.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/HyperVExtension/src/DevSetupEngineProjection/DevSetupEngineProjection.csproj b/HyperVExtension/src/DevSetupEngineProjection/DevSetupEngineProjection.csproj index b44bff1bf..b0e491b69 100644 --- a/HyperVExtension/src/DevSetupEngineProjection/DevSetupEngineProjection.csproj +++ b/HyperVExtension/src/DevSetupEngineProjection/DevSetupEngineProjection.csproj @@ -6,7 +6,7 @@ <!-- Workaround for MSB3271 error on processor architecture mismatch --> <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> </PropertyGroup> <ItemGroup> diff --git a/HyperVExtension/src/HyperVExtension.Common/HyperVExtension.Common.csproj b/HyperVExtension/src/HyperVExtension.Common/HyperVExtension.Common.csproj index 0efc2965e..cc17ff4aa 100644 --- a/HyperVExtension/src/HyperVExtension.Common/HyperVExtension.Common.csproj +++ b/HyperVExtension/src/HyperVExtension.Common/HyperVExtension.Common.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>HyperVExtension.Common</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> </PropertyGroup> <ItemGroup> diff --git a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs similarity index 66% rename from HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs rename to HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs index f812115e9..c64c211b3 100644 --- a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/HResultException.cs +++ b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HResultException.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace HyperVExtension.CommunicationWithGuest; +namespace HyperVExtension.HostGuestCommunication; -internal sealed class HResultException : Exception +public sealed class HResultException : Exception { public HResultException(int resultCode, string? description = null) : base(description) diff --git a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HyperVExtension.HostGuestCommunication.csproj b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HyperVExtension.HostGuestCommunication.csproj index c557df952..d59ecbc4c 100644 --- a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HyperVExtension.HostGuestCommunication.csproj +++ b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/HyperVExtension.HostGuestCommunication.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>HyperVExtension.HostGuestCommunication</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> diff --git a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs index 8ec2c1ee1..6d8401907 100644 --- a/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs +++ b/HyperVExtension/src/HyperVExtension.HostGuestCommunication/Providers/MessageHelper.cs @@ -110,7 +110,7 @@ public static Dictionary<string, string> MergeMessageParts(Dictionary<string, st catch (Exception ex) { var log = Serilog.Log.ForContext("SourceContext", nameof(MessageHelper)); - log.Error($"Could not read guest message {valueName}", ex); + log.Error(ex, $"Could not read guest message {valueName}"); } messages.Add(name, value); diff --git a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/ApplyConfigurationOperation.cs b/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/ApplyConfigurationOperation.cs index 08c70efa3..5b8c3165e 100644 --- a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/ApplyConfigurationOperation.cs +++ b/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/ApplyConfigurationOperation.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using HyperVExtension.HostGuestCommunication; using HyperVExtension.Models; using Microsoft.Windows.DevHome.SDK; using Windows.Foundation; diff --git a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/Responses/ResponseFactory.cs b/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/Responses/ResponseFactory.cs index 1bf559c4c..0a0a4fd6e 100644 --- a/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/Responses/ResponseFactory.cs +++ b/HyperVExtension/src/HyperVExtension/CommunicationWithGuest/Responses/ResponseFactory.cs @@ -66,7 +66,7 @@ public IGuestResponse CreateResponse(IResponseMessage message) { var messageId = message?.ResponseId ?? "<unknown>"; var responseData = message?.ResponseData ?? "<unknown>"; - _log.Error($"Error processing message. Message ID: {messageId}. Request data: {responseData}", ex); + _log.Error(ex, $"Error processing message. Message ID: {messageId}. Request data: {responseData}"); return new ErrorResponse(message!); } } diff --git a/HyperVExtension/src/HyperVExtension/Constants.cs b/HyperVExtension/src/HyperVExtension/Constants.cs index 8a9971b66..378dbe17c 100644 --- a/HyperVExtension/src/HyperVExtension/Constants.cs +++ b/HyperVExtension/src/HyperVExtension/Constants.cs @@ -22,4 +22,6 @@ internal sealed class Constants #else public const string ExtensionIcon = "ms-resource://Microsoft.Windows.DevHome.Dev/Files/HyperVExtension/Assets/hyper-v-provider-icon.png"; #endif + + public const string ExtensionIconInternal = "ms-appx:///HyperVExtension/Assets/hyper-v-provider-icon.png"; } diff --git a/HyperVExtension/src/HyperVExtension/Exceptions/AdaptiveCardInvalidActionException.cs b/HyperVExtension/src/HyperVExtension/Exceptions/AdaptiveCardInvalidActionException.cs new file mode 100644 index 000000000..38c73aedd --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Exceptions/AdaptiveCardInvalidActionException.cs @@ -0,0 +1,18 @@ +// 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; + +namespace HyperVExtension.Exceptions; + +public class AdaptiveCardInvalidActionException : Exception +{ + public AdaptiveCardInvalidActionException(string message) + : base(message) + { + } +} diff --git a/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs b/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs index a1af58afa..c44703db1 100644 --- a/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs +++ b/HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using HyperVExtension.Models; namespace HyperVExtension.Extensions; @@ -19,10 +20,11 @@ public static class StreamExtensions /// <param name="progressProvider">The object that progress will be reported to</param> /// <param name="bufferSize">The size of the buffer which is used to read data from the source stream and write it to the destination stream</param> /// <param name="cancellationToken">A cancellation token that will allow the caller to cancel the operation</param> - public static async Task CopyToAsync(this Stream source, Stream destination, IProgress<long> progressProvider, int bufferSize, CancellationToken cancellationToken) + public static async Task CopyToAsync(this Stream source, Stream destination, IProgress<ByteTransferProgress> progressProvider, int bufferSize, long totalBytesToExtract, CancellationToken cancellationToken) { var buffer = new byte[bufferSize]; long totalRead = 0; + var lastPercentage = 0U; while (true) { @@ -37,8 +39,15 @@ public static async Task CopyToAsync(this Stream source, Stream destination, IPr await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); totalRead += bytesRead; - // Report the progress of the operation. - progressProvider.Report(totalRead); + var progressPercentage = (uint)(totalRead / (double)totalBytesToExtract * 100D); + + // Only update progress when a whole percentage has been completed. + if (progressPercentage != lastPercentage) + { + // Report the progress of the operation. + progressProvider.Report(new ByteTransferProgress(totalRead, totalBytesToExtract)); + lastPercentage = progressPercentage; + } } } } diff --git a/HyperVExtension/src/HyperVExtension/Helpers/AdaptiveCardActionPayload.cs b/HyperVExtension/src/HyperVExtension/Helpers/AdaptiveCardActionPayload.cs index 3b3d4f1b3..d4bc237b7 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/AdaptiveCardActionPayload.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/AdaptiveCardActionPayload.cs @@ -30,6 +30,11 @@ public string? Type get; set; } + public string? Mode + { + get; set; + } + public bool IsCancelAction() { return Id == "cancelAction"; diff --git a/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs b/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs index cb6ccaeaf..b5796b7cc 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/DevSetupAgentDeploymentHelper.cs @@ -3,6 +3,7 @@ using System.Management.Automation; using System.Security; +using System.Text; using HyperVExtension.Exceptions; using HyperVExtension.Services; @@ -28,6 +29,7 @@ private enum ProcessorArchitecture : ushort private readonly IPowerShellService _powerShellService; private readonly string _vmId; + private readonly Lazy<string> _script = new(() => LoadScript()); public DevSetupAgentDeploymentHelper(IPowerShellService powerShellService, string vmId) { @@ -43,7 +45,7 @@ public void DeployDevSetupAgent(string userName, SecureString password) var sourcePath = GetSourcePath(architecture); var deployDevSetupAgentStatement = new StatementBuilder() - .AddScript(_script, false) + .AddScript(_script.Value, false) .AddCommand("Install-DevSetupAgent") .AddParameter("VMId", _vmId) .AddParameter("Session", session) @@ -124,203 +126,9 @@ private ushort GetVMArchitechture(PSObject session) return (ushort)psObject.BaseObject; } - private readonly string _script = @" -function Install-DevSetupAgent -{ - Param( - [Parameter(Mandatory = $true)] - [Guid] $VMId, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.Runspaces.PSSession] $Session, - - [Parameter(Mandatory = $true)] - [string] $Path - ) - - $ErrorActionPreference = ""Stop"" - $activity = ""Installing DevSetupAgent to VM $VMId"" - - # Validate input. Only .cab and .zip files are supported - # If $Path is a directory, it will be copied to the VM and installed as is - $isDirectory = $false - $isCab = $false - $inputFileName = $null - if (Test-Path -Path $Path -PathType 'Container') - { - $isDirectory = $true - } - elseif (Test-Path -Path $Path -PathType 'Leaf') - { - if ($Path -match '\.(cab)$') - { - $isCab = $true - } - elseif (-not $Path -match '\.(zip)$') - { - throw ""Only .cab and .zip files are supported"" - } - $inputFileName = Split-Path -Path $Path -Leaf - } - else - { - throw ""$Path does not exist"" - } - - - $DevSetupAgentConst = ""DevSetupAgent"" - $DevSetupEngineConst = ""DevSetupEngine"" - $session = $Session - - $guestTempDirectory = Invoke-Command -Session $session -ScriptBlock { $env:temp } - - [string] $guid = [System.Guid]::NewGuid() - $guestUnpackDirectory = Join-Path -Path $guestTempDirectory -ChildPath $guid - $guestDevSetupAgentTempDirectory = Join-Path -Path $guestUnpackDirectory -ChildPath $DevSetupAgentConst - - Write-Host ""Creating VM temporary folder $guestUnpackDirectory"" - Write-Progress -Activity $activity -Status ""Creating VM temporary folder $guestUnpackDirectory"" -PercentComplete 10 - Invoke-Command -Session $session -ScriptBlock { New-Item -Path ""$using:guestUnpackDirectory"" -ItemType ""directory"" } - - if ($isDirectory) - { - $destinationPath = $guestDevSetupAgentTempDirectory - } - else - { - $destinationPath = $guestUnpackDirectory - } - - Write-Host ""Copying $Path to VM $destinationPath"" - Write-Progress -Activity $activity -Status ""Copying DevSetupAgent to VM $destinationPath"" -PercentComplete 15 - Copy-Item -ToSession $session -Recurse -Path $Path -Destination $destinationPath - - - Invoke-Command -Session $session -ScriptBlock { - $ErrorActionPreference = ""Stop"" - - try - { - $guestDevSetupAgentPath = Join-Path -Path $Env:Programfiles -ChildPath $using:DevSetupAgentConst - - # Stop and remove previous version of DevSetupAgent service if it exists - $service = Get-Service -Name $using:DevSetupAgentConst -ErrorAction SilentlyContinue - if ($service) - { - $serviceWMI = Get-WmiObject -Class Win32_Service -Filter ""Name='$using:DevSetupAgentConst'"" - $existingServicePath = $serviceWMI.Properties[""PathName""].Value - if ($existingServicePath) - { - $guestDevSetupAgentPath = Split-Path $existingServicePath -Parent - } - - try - { - Write-Host ""Stopping DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Stopping DevSetupAgent service $destinationPath"" -PercentComplete 30 - $service.Stop() - } - catch - { - Write-Host ""Ignoring error: $PSItem"" - } - - Remove-Variable -Name service -ErrorAction SilentlyContinue - - # Remove-Service is only available in PowerShell 6.0 and later. Windows doesn't come with it preinstalled. - Write-Host ""Removing DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Removing DevSetupAgent service"" -PercentComplete 35 - $serviceWMI = Get-WmiObject -Class Win32_Service -Filter ""Name='$using:DevSetupAgentConst'"" - $serviceWMI.Delete() - Remove-Variable -Name serviceWMI -ErrorAction SilentlyContinue - } - - # Stop previous version of DevSetupEngine COM server if it exists - $devSetupEngineProcess = Get-Process -Name ""$using:DevSetupEngineConst"" -ErrorAction SilentlyContinue - if ($devSetupEngineProcess -ne $null) - { - Write-Host ""Stopping $using:DevSetupEngineConst process"" - Write-Progress -Activity $using:activity -Status ""Stopping $using:DevSetupEngineConst process"" -PercentComplete 40 - Stop-Process -Force -Name ""$using:DevSetupEngineConst"" - } - - # Unregister DevSetupEngine - $enginePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath ""$using:DevSetupEngineConst.exe"" - if (Test-Path -Path $enginePath) - { - Write-Host ""Unregistering DevSetupEngine ($enginePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupEngine ($enginePath)"" -PercentComplete 88 - &$enginePath ""-UnregisterComServer"" - } - - # Remove previous version of DevSetupAgent service files - if (Test-Path -Path $guestDevSetupAgentPath) - { - # Sleep a few seconds to make sure all handles released after shutting down previous DevSetupEngine - Start-Sleep -Seconds 7 - Write-Host ""Deleting old DevSetupAgent service files"" - Write-Progress -Activity $using:activity -Status ""Deleting old DevSetupAgent service files"" -PercentComplete 45 - Remove-Item -Recurse -Force -Path $guestDevSetupAgentPath - } - - if ($using:isDirectory) - { - Write-Host ""Copying DevSetupAgent to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Deleting old DevSetupAgent service files"" -PercentComplete 50 - Copy-Item -Recurse -Path $using:guestDevSetupAgentTempDirectory -Destination $guestDevSetupAgentPath - } - elseif ($using:isCab) - { - $cabPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName - Write-Host ""Unpacking $cabPath to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Unpacking $cabPath to $guestDevSetupAgentPath"" -PercentComplete 60 - $expandOutput=&""$Env:SystemRoot\System32\expand.exe"" $cabPath /F:* $Env:Programfiles - if ($LastExitCode -ne 0) - { - throw ""Error unpacking $cabPath`:`n$LastExitCode`n$($expandOutput|Out-String)"" - } - } - else - { - $zipPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName - Write-Host ""Unpacking $using:inputFileName to $guestDevSetupAgentPath"" - Write-Progress -Activity $using:activity -Status ""Unpacking $using:inputFileName to $guestDevSetupAgentPath"" -PercentComplete 60 - Expand-Archive -Path $zipPath -Destination $guestDevSetupAgentPath - } - - # Register DevSetupAgent service - $servicePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath ""$using:DevSetupAgentConst.exe"" - Write-Host ""Registering DevSetupAgent service ($servicePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupAgent service ($servicePath)"" -PercentComplete 85 - New-Service -Name $using:DevSetupAgentConst -BinaryPathName $servicePath -StartupType Automatic - - # Register DevSetupEngine - Write-Host ""Registering DevSetupEngine ($enginePath)"" - Write-Progress -Activity $using:activity -Status ""Registering DevSetupEngine ($enginePath)"" -PercentComplete 88 - - # Executing non-console apps using '&' does not set $LastExitCode. Using Start-Process here to get the returned error code. - $process = Start-Process -NoNewWindow -Wait $enginePath -ArgumentList ""-RegisterComServer"" -PassThru - if ($process.ExitCode -ne 0) - { - throw ""Error registering $enginePath`: $process.ExitCode"" - } - - Write-Host ""Starting DevSetupAgent service"" - Write-Progress -Activity $using:activity -Status ""Starting DevSetupAgent service"" -PercentComplete 92 - Start-Service $using:DevSetupAgentConst - } - catch - { - Write-Host ""Error on guest OS: $PSItem"" - } - finally - { - Write-Host ""Removing temporary directory $using:guestUnpackDirectory"" - Remove-Item -Recurse -Force -Path $using:guestUnpackDirectory -ErrorAction SilentlyContinue - } - } - - Remove-PSSession $session -} -"; + private static string LoadScript() + { + var path = Path.Combine(AppContext.BaseDirectory, "HyperVExtension", "Scripts", "DevSetupAgent.ps1"); + return File.ReadAllText(path, Encoding.Default) ?? throw new FileNotFoundException(path); + } } diff --git a/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs b/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs index 3c88fd838..e3a171360 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/PsObjectHelper.cs @@ -71,7 +71,7 @@ public PsObjectHelper(in PSObject pSObject) catch (Exception ex) { var log = Log.ForContext("SourceContext", nameof(PsObjectHelper)); - log.Error($"Failed to get property value with name {propertyName} from object with type {type}.", ex); + log.Error(ex, $"Failed to get property value with name {propertyName} from object with type {type}."); } return default(T); diff --git a/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs b/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs index 570a98ecf..eeb03be0e 100644 --- a/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs +++ b/HyperVExtension/src/HyperVExtension/Helpers/Resources.cs @@ -23,7 +23,7 @@ public static string GetResource(string identifier, ILogger? log = null) } catch (Exception ex) { - log?.Error($"Failed loading resource: {identifier}", ex); + log?.Error(ex, $"Failed loading resource: {identifier}"); // If we fail, load the original identifier so it is obvious which resource is missing. return identifier; diff --git a/HyperVExtension/src/HyperVExtension/HyperVExtension.cs b/HyperVExtension/src/HyperVExtension/HyperVExtension.cs index 3bc6f6a09..e71c41124 100644 --- a/HyperVExtension/src/HyperVExtension/HyperVExtension.cs +++ b/HyperVExtension/src/HyperVExtension/HyperVExtension.cs @@ -57,7 +57,7 @@ public HyperVExtension(IHost host) } catch (Exception ex) { - log.Error($"Failed to get provider for provider type {providerType}", ex); + log.Error(ex, $"Failed to get provider for provider type {providerType}"); } return provider; diff --git a/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj b/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj index 446faa6e5..4e3f564cf 100644 --- a/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj +++ b/HyperVExtension/src/HyperVExtension/HyperVExtension.csproj @@ -5,13 +5,17 @@ <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <BuildRing Condition="'$(BuildRing)'==''">Dev</BuildRing> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> </PropertyGroup> <ItemGroup> + <None Remove="Scripts\DevSetupAgent.ps1" /> <None Remove="Templates\VmCredentialAdaptiveCardTemplate.json" /> <None Remove="Templates\WaitForLoginAdaptiveCardTemplate.json" /> </ItemGroup> <ItemGroup> + <Content Include="Scripts\DevSetupAgent.ps1"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> <Content Include="Templates\VmCredentialAdaptiveCardTemplate.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> @@ -19,7 +23,14 @@ <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> - + <ItemGroup> + <Content Include="Templates\InitialVMGalleryCreationForm.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + <Content Include="Templates\ReviewFormForVMGallery.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </Content> + </ItemGroup> <ItemGroup> <PackageReference Include="MessageFormat" Version="6.0.2" /> <PackageReference Include="Microsoft.Management.Infrastructure" Version="3.0.0" /> diff --git a/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs b/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs new file mode 100644 index 000000000..1cb1cf2b5 --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs @@ -0,0 +1,28 @@ +// 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; + +namespace HyperVExtension.Models; + +/// <summary> +/// Represents progress of an operation that require transferring bytes from one place to another. +/// </summary> +public class ByteTransferProgress +{ + public long BytesReceived { get; set; } + + public long TotalBytesToReceive { get; set; } + + public uint PercentageComplete => (uint)((BytesReceived / (double)TotalBytesToReceive) * 100); + + public ByteTransferProgress(long bytesReceived, long totalBytesToReceive) + { + BytesReceived = bytesReceived; + TotalBytesToReceive = totalBytesToReceive; + } +} diff --git a/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs b/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs index eccfa1458..0e190d4bc 100644 --- a/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs +++ b/HyperVExtension/src/HyperVExtension/Models/HyperVVirtualMachine.cs @@ -192,7 +192,7 @@ private ComputeSystemOperationResult Start(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Start), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Start)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } } @@ -253,7 +253,7 @@ public IAsyncOperation<ComputeSystemOperationResult> TerminateAsync(string optio catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Terminate), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Terminate)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -278,7 +278,7 @@ public IAsyncOperation<ComputeSystemOperationResult> DeleteAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Delete), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Delete)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -309,7 +309,7 @@ public IAsyncOperation<ComputeSystemOperationResult> SaveAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Save), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Save)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -340,7 +340,7 @@ public IAsyncOperation<ComputeSystemOperationResult> PauseAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Pause), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Pause)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -371,7 +371,7 @@ public IAsyncOperation<ComputeSystemOperationResult> ResumeAsync(string options) catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Resume), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Resume)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -393,7 +393,7 @@ public IAsyncOperation<ComputeSystemOperationResult> CreateSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.CreateSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.CreateSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -416,7 +416,7 @@ public IAsyncOperation<ComputeSystemOperationResult> RevertSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.RevertSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.RevertSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -439,7 +439,7 @@ public IAsyncOperation<ComputeSystemOperationResult> DeleteSnapshotAsync(string } catch (Exception ex) { - _log.Error(OperationErrorString(ComputeSystemOperations.DeleteSnapshot), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.DeleteSnapshot)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -457,7 +457,7 @@ public IAsyncOperation<ComputeSystemOperationResult> ConnectAsync(string options } catch (Exception ex) { - _log.Error($"Failed to launch vmconnect on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to launch vmconnect on {DateTime.Now}: VM details: {this}"); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -487,7 +487,7 @@ public IAsyncOperation<ComputeSystemOperationResult> RestartAsync(string options catch (Exception ex) { StateChanged(this, ComputeSystemState.Unknown); - _log.Error(OperationErrorString(ComputeSystemOperations.Restart), ex); + _log.Error(ex, OperationErrorString(ComputeSystemOperations.Restart)); return new ComputeSystemOperationResult(ex, OperationErrorUnknownString, ex.Message); } }).AsAsyncOperation(); @@ -536,7 +536,7 @@ public IAsyncOperation<IEnumerable<ComputeSystemProperty>> GetComputeSystemPrope } catch (Exception ex) { - _log.Error($"Failed to GetComputeSystemPropertiesAsync on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to GetComputeSystemPropertiesAsync on {DateTime.Now}: VM details: {this}"); return new List<ComputeSystemProperty>(); } }).AsAsyncOperation(); @@ -708,7 +708,7 @@ public SDK.ApplyConfigurationResult ApplyConfiguration(ApplyConfigurationOperati } catch (Exception ex) { - _log.Error($"Failed to apply configuration on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to apply configuration on {DateTime.Now}: VM details: {this}"); return operation.CompleteOperation(new HostGuestCommunication.ApplyConfigurationResult(ex.HResult, ex.Message)); } } @@ -721,7 +721,7 @@ public SDK.ApplyConfigurationResult ApplyConfiguration(ApplyConfigurationOperati } catch (Exception ex) { - _log.Error($"Failed to apply configuration on {DateTime.Now}: VM details: {this}", ex); + _log.Error(ex, $"Failed to apply configuration on {DateTime.Now}: VM details: {this}"); return new ApplyConfigurationOperation(this, ex); } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs new file mode 100644 index 000000000..7136b91b3 --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Models/VMGalleryCreationAdaptiveCardSession.cs @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.InteropServices.WindowsRuntime; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using HyperVExtension.Common; +using HyperVExtension.Exceptions; +using HyperVExtension.Helpers; +using HyperVExtension.Models.VirtualMachineCreation; +using HyperVExtension.Models.VMGalleryJsonToClasses; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation; +using Windows.Storage; +using Windows.Storage.Streams; + +namespace HyperVExtension.Models; + +public enum SessionState +{ + InitialCreationForm, + ReviewForm, +} + +public class VMGalleryCreationAdaptiveCardSession : IExtensionAdaptiveCardSession2 +{ + private readonly Serilog.ILogger _log = Log.ForContext("SourceContext", nameof(HyperVVirtualMachine)); + + private readonly string _pathToInitialCreationFormTemplate = Path.Combine(AppContext.BaseDirectory, @"HyperVExtension\Templates\", "InitialVMGalleryCreationForm.json"); + + private readonly string _pathToReviewFormTemplate = Path.Combine(AppContext.BaseDirectory, @"HyperVExtension\Templates\", "ReviewFormForVMGallery.json"); + + private readonly string _adaptiveCardNextButtonId = "DevHomeMachineConfigurationNextButton"; + + /// <summary> + /// The gallery images that will be displayed in the initial creation form. We retrieve these from the VMGallery.json file in the Microsoft servers. + /// </summary> + private readonly VMGalleryImageList _vMGalleryImageList; + + private readonly IStringResource _stringResource; + + /// <summary> + /// Gets the Json string that represents the user input that was passed to the adaptive card session. We'll keep this so we can pass it back to Dev Home + /// at the end of the session. + /// </summary> + public string OriginalUserInputJson { get; private set; } = string.Empty; + + public event TypedEventHandler<IExtensionAdaptiveCardSession2, ExtensionAdaptiveCardSessionStoppedEventArgs>? Stopped; + + public void Dispose() + { + } + + public VMGalleryCreationAdaptiveCardSession(VMGalleryImageList galleryImages, IStringResource stringResource) + { + _vMGalleryImageList = galleryImages; + _stringResource = stringResource; + } + + private IExtensionAdaptiveCard? _creationAdaptiveCard; + + public bool ShouldEndSession { get; private set; } + + public ProviderOperationResult Initialize(IExtensionAdaptiveCard extensionUI) + { + _creationAdaptiveCard = extensionUI; + + return GetInitialCreationFormAdaptiveCard(); + } + + public IAsyncOperation<ProviderOperationResult> OnAction(string action, string inputs) + { + return Task.Run(async () => + { + ProviderOperationResult operationResult; + var shouldEndSession = false; + var adaptiveCardStateNotRecognizedError = _stringResource.GetLocalized("AdaptiveCardStateNotRecognizedError"); + + var actionPayload = Helpers.Json.ToObject<AdaptiveCardActionPayload>(action); + if (actionPayload == null) + { + _log.Error($"Actions in Adaptive card action Json not recognized: {action}"); + var creationFormGenerationError = _stringResource.GetLocalized("AdaptiveCardUnRecognizedAction"); + var exception = new AdaptiveCardInvalidActionException(creationFormGenerationError); + return new ProviderOperationResult(ProviderOperationStatus.Failure, exception, creationFormGenerationError, creationFormGenerationError); + } + + switch (_creationAdaptiveCard?.State) + { + case "initialCreationForm": + operationResult = await HandleActionWhenFormInInitialState(actionPayload, inputs); + break; + case "reviewForm": + (operationResult, shouldEndSession) = await HandleActionWhenFormInReviewState(actionPayload); + break; + default: + shouldEndSession = true; + operationResult = new ProviderOperationResult( + ProviderOperationStatus.Failure, + new InvalidOperationException(nameof(action)), + adaptiveCardStateNotRecognizedError, + $"Unexpected state:{_creationAdaptiveCard?.State}"); + break; + } + + if (shouldEndSession) + { + // The session has now ended. We'll raise the Stopped event to notify anyone in Dev Home who was listening to this event, + // that the session has ended. + Stopped?.Invoke( + this, + new ExtensionAdaptiveCardSessionStoppedEventArgs(operationResult, OriginalUserInputJson)); + } + + return operationResult; + }).AsAsyncOperation(); + } + + /// <summary> + /// Loads the adaptive card template based on the session state. + /// </summary> + /// <param name="state">State the adaptive card session</param> + /// <returns>A Json string representing the adaptive card</returns> + public string LoadTemplate(SessionState state) + { + var pathToTemplate = state switch + { + SessionState.InitialCreationForm => _pathToInitialCreationFormTemplate, + SessionState.ReviewForm => _pathToReviewFormTemplate, + _ => _pathToInitialCreationFormTemplate, + }; + + return File.ReadAllText(pathToTemplate, Encoding.Default); + } + + /// <summary> + /// Creates the initial form that will be displayed to the user. It will be a list of Windows community toolkit's settings + /// cards that can be displayed in the Dev Homes UI. + /// </summary> + /// <returns>Result of the operation</returns> + private ProviderOperationResult GetInitialCreationFormAdaptiveCard() + { + try + { + // Create the JSON array for the gallery images and add the data for each image. + // these will be display in the initial creation form. + var jsonArrayOfGalleryImages = new JsonArray(); + var primaryButtonForCreationFlowText = _stringResource.GetLocalized("PrimaryButtonLabelForCreationFlow"); + var secondaryButtonForCreationFlowText = _stringResource.GetLocalized("SecondaryButtonLabelForCreationFlow"); + var secondaryButtonForContentDialogText = _stringResource.GetLocalized("SecondaryButtonForContentDialogText"); + var buttonToLaunchContentDialogLabel = _stringResource.GetLocalized("ButtonToLaunchContentDialogLabel"); + var settingsCardLabel = _stringResource.GetLocalized("SettingsCardLabel"); + var enterNewVMNameLabel = _stringResource.GetLocalized("EnterNewVMNameLabel"); + var enterNewVMNamePlaceHolder = _stringResource.GetLocalized("EnterNewVMNamePlaceHolder"); + + foreach (var image in _vMGalleryImageList.Images) + { + var dataJson = new JsonObject + { + { "ImageDescription", GetMergedDescription(image) }, + { "SubDescription", image.Publisher }, + { "Header", image.Name }, + { "HeaderIcon", image.Symbol.Base64Image }, + { "ActionButtonText", "More info" }, + { "ContentDialogInfo", SetupContentDialogInfo(image) }, + { "ButtonToLaunchContentDialogLabel", buttonToLaunchContentDialogLabel }, + { "SecondaryButtonForContentDialogText", secondaryButtonForContentDialogText }, + }; + + jsonArrayOfGalleryImages.Add(dataJson); + } + + var templateData = + $"{{\"PrimaryButtonLabelForCreationFlow\" : \"{primaryButtonForCreationFlowText}\"," + + $"\"SecondaryButtonLabelForCreationFlow\" : \"{secondaryButtonForCreationFlowText}\"," + + $"\"SettingsCardLabel\": \"{settingsCardLabel}\"," + + $"\"EnterNewVMNameLabel\": \"{enterNewVMNameLabel}\"," + + $"\"EnterNewVMNamePlaceHolder\": \"{enterNewVMNamePlaceHolder}\"," + + $"\"GalleryImages\" : {jsonArrayOfGalleryImages.ToJsonString()}" + + $"}}"; + + var template = LoadTemplate(SessionState.InitialCreationForm); + + return _creationAdaptiveCard!.Update(template, templateData, "initialCreationForm"); + } + catch (Exception ex) + { + var creationFormGenerationError = _stringResource.GetLocalized("InitialCreationFormGenerationFailedError"); + return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, creationFormGenerationError, ex.Message); + } + } + + /// <summary> + /// Creates the review form that will be displayed to the user. This will be an adaptive card that is displayed in Dev Homes + /// setup flow review page. + /// </summary> + /// <returns>Result of the operation</returns> + private async Task<ProviderOperationResult> GetForReviewFormAdaptiveCardAsync(string inputJson) + { + try + { + var deserializedObject = JsonSerializer.Deserialize(inputJson, typeof(VMGalleryCreationUserInput)); + var inputForGalleryOperation = deserializedObject as VMGalleryCreationUserInput ?? throw new InvalidOperationException($"Json deserialization failed for input Json: {inputJson}"); + + if (inputForGalleryOperation.SelectedImageListIndex < 0 || + inputForGalleryOperation.SelectedImageListIndex > _vMGalleryImageList.Images.Count) + { + return new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Failed to get review form", "Selected image index is out of range"); + } + + var galleryImage = _vMGalleryImageList.Images[inputForGalleryOperation.SelectedImageListIndex]; + var newEnvironmentNameLabel = _stringResource.GetLocalized("NameLabelForNewVirtualMachine", ":"); + var primaryButtonForCreationFlowText = _stringResource.GetLocalized("PrimaryButtonLabelForCreationFlow"); + var secondaryButtonForCreationFlowText = _stringResource.GetLocalized("SecondaryButtonLabelForCreationFlow"); + var storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(Constants.ExtensionIconInternal)); + var randomAccessStream = await storageFile.OpenReadAsync(); + + // Convert the stream to a byte array + var bytes = new byte[randomAccessStream.Size]; + await randomAccessStream.ReadAsync(bytes.AsBuffer(), (uint)randomAccessStream.Size, InputStreamOptions.None); + var providerBase64Image = Convert.ToBase64String(bytes); + var reviewFormData = new JsonObject + { + { "ProviderName", HyperVStrings.HyperVProviderDisplayName }, + { "DiskImageSize", BytesHelper.ConvertBytesToString(galleryImage.Disk.SizeInBytes) }, + { "VMGalleryImageName", galleryImage.Name }, + { "Publisher", galleryImage.Publisher }, + { "NameOfNewVM", inputForGalleryOperation.NewEnvironmentName }, + { "NameLabel", newEnvironmentNameLabel }, + { "Base64ImageForProvider", providerBase64Image }, + { "DiskImageUrl", galleryImage.Symbol.Uri }, + { "PrimaryButtonLabelForCreationFlow", primaryButtonForCreationFlowText }, + { "SecondaryButtonLabelForCreationFlow", secondaryButtonForCreationFlowText }, + }; + + var template = LoadTemplate(SessionState.ReviewForm); + + return _creationAdaptiveCard!.Update(LoadTemplate(SessionState.ReviewForm), reviewFormData.ToJsonString(), "reviewForm"); + } + catch (Exception ex) + { + var reviewFormGenerationError = _stringResource.GetLocalized("ReviewFormGenerationFailedError"); + return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, reviewFormGenerationError, ex.Message); + } + } + + /// <summary> + /// The description for VM gallery images is stored in a list of strings. This method merges the strings into one string. + /// </summary> + /// <param name="image">The c# class that represents the gallery image</param> + /// <returns>A string that combines the original list of strings into one</returns> + public string GetMergedDescription(VMGalleryImage image) + { + var description = string.Empty; + for (var i = 0; i < image.Description.Count; i++) + { + description += image.Description[i].Replace("\n", string.Empty).Replace("\r", string.Empty); + } + + return string.Join(string.Empty, description); + } + + /// <summary> + /// In Dev Homes UI, the user can click a more info button to get more information about the VM gallery image. + /// This method sets up the content dialog's body data that will be displayed when the user clicks the more info button. + /// </summary> + /// <param name="image">The c# class that represents the gallery image</param> + /// <returns>A Json object that contains the data needed to display an adaptive card within a content dialogs body</returns> + private JsonObject SetupContentDialogInfo(VMGalleryImage image) + { + var adaptiveCardImageFacts = new JsonArray(); + foreach (var fact in image.Details) + { + var adaptiveCardfactObj = new JsonObject + { + { "title", fact.Name }, + { "value", fact.Value }, + }; + adaptiveCardImageFacts.Add(adaptiveCardfactObj); + } + + var osVersionForContentDialog = _stringResource.GetLocalized("OsVersionForContentDialog"); + var localeForContentDialog = _stringResource.GetLocalized("LocaleForContentDialog"); + var lastUpdatedForContentDialog = _stringResource.GetLocalized("LastUpdatedForContentDialog"); + var downloadForContentDialog = _stringResource.GetLocalized("DownloadForContentDialog"); + + adaptiveCardImageFacts.Add(new JsonObject() { { "title", osVersionForContentDialog }, { "value", image.Version } }); + adaptiveCardImageFacts.Add(new JsonObject() { { "title", localeForContentDialog }, { "value", image.Locale } }); + adaptiveCardImageFacts.Add(new JsonObject() { { "title", lastUpdatedForContentDialog }, { "value", image.LastUpdated.ToLongDateString() } }); + adaptiveCardImageFacts.Add(new JsonObject() { { "title", downloadForContentDialog }, { "value", BytesHelper.ConvertBytesToString(image.Disk.SizeInBytes) } }); + + return new JsonObject + { + { "GalleryImageFacts", adaptiveCardImageFacts }, + { "ImageDescription", GetMergedDescription(image) }, + }; + } + + private async Task<ProviderOperationResult> HandleActionWhenFormInInitialState(AdaptiveCardActionPayload actionPayload, string inputs) + { + ProviderOperationResult operationResult; + var actionButtonId = actionPayload.Id ?? string.Empty; + + if (actionButtonId.Equals(_adaptiveCardNextButtonId, StringComparison.OrdinalIgnoreCase)) + { + // if OnAction's state is initialCreationForm, then the user has selected a VM gallery image and is ready to review the form. + // we'll also keep the original user input so we can pass it back to Dev Home once the session ends. + OriginalUserInputJson = inputs; + operationResult = await GetForReviewFormAdaptiveCardAsync(inputs); + } + else + { + operationResult = GetInitialCreationFormAdaptiveCard(); + } + + return operationResult; + } + + private async Task<(ProviderOperationResult, bool)> HandleActionWhenFormInReviewState(AdaptiveCardActionPayload actionPayload) + { + ProviderOperationResult operationResult; + var shouldEndSession = false; + var actionButtonId = actionPayload.Id ?? string.Empty; + + if (actionButtonId.Equals(_adaptiveCardNextButtonId, StringComparison.OrdinalIgnoreCase)) + { + // if OnAction's state is reviewForm, then the user has reviewed the form and Dev Home has started the creation process. + // we'll show the same form to the user in Dev Homes summary page. + shouldEndSession = true; + operationResult = await GetForReviewFormAdaptiveCardAsync(OriginalUserInputJson); + } + else + { + operationResult = GetInitialCreationFormAdaptiveCard(); + } + + return (operationResult, shouldEndSession); + } +} diff --git a/HyperVExtension/src/HyperVExtension/Models/VMGalleryJsonToClasses/VMGalleryDisk.cs b/HyperVExtension/src/HyperVExtension/Models/VMGalleryJsonToClasses/VMGalleryDisk.cs index f8078cfcf..9bf8594af 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VMGalleryJsonToClasses/VMGalleryDisk.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VMGalleryJsonToClasses/VMGalleryDisk.cs @@ -10,5 +10,5 @@ public sealed class VMGalleryDisk : VMGalleryItemWithHashBase { public string ArchiveRelativePath { get; set; } = string.Empty; - public long SizeInBytes { get; set; } + public ulong SizeInBytes { get; set; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs index c5c5657e1..e606dacf6 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/ArchiveExtractionReport.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; + namespace HyperVExtension.Models.VirtualMachineCreation; /// <summary> @@ -10,15 +12,12 @@ public sealed class ArchiveExtractionReport : IOperationReport { public ReportKind ReportKind => ReportKind.ArchiveExtraction; - public string LocalizationKey => "ExtractingFile"; - - public ulong BytesReceived { get; private set; } + public string LocalizationKey => "ExtractionInProgress"; - public ulong TotalBytesToReceive { get; private set; } + public ByteTransferProgress ProgressObject { get; private set; } - public ArchiveExtractionReport(ulong bytesReceived, ulong totalBytesToReceive) + public ArchiveExtractionReport(ByteTransferProgress progressObj) { - BytesReceived = bytesReceived; - TotalBytesToReceive = totalBytesToReceive; + ProgressObject = progressObj; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs index 8c76087c4..073a2822e 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DotNetZipArchiveProvider.cs @@ -28,13 +28,13 @@ public async Task ExtractArchiveAsync(IProgress<IOperationReport> progressProvid using var outputFileStream = File.OpenWrite(destinationAbsoluteFilePath); using var zipArchiveEntryStream = zipArchiveEntry.Open(); - var fileExtractionProgress = new Progress<long>(bytesCopied => + var fileExtractionProgress = new Progress<ByteTransferProgress>(progressObj => { - progressProvider.Report(new ArchiveExtractionReport((ulong)bytesCopied, (ulong)totalBytesToExtract)); + progressProvider.Report(new ArchiveExtractionReport(progressObj)); }); outputFileStream.SetLength(totalBytesToExtract); - await zipArchiveEntryStream.CopyToAsync(outputFileStream, fileExtractionProgress, _transferBufferSize, cancellationToken); + await zipArchiveEntryStream.CopyToAsync(outputFileStream, fileExtractionProgress, _transferBufferSize, totalBytesToExtract, cancellationToken); File.SetLastWriteTime(destinationAbsoluteFilePath, zipArchiveEntry.LastWriteTime.DateTime); } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs index abec4f630..53fe90626 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/DownloadOperationReport.cs @@ -9,13 +9,10 @@ public class DownloadOperationReport : IOperationReport public string LocalizationKey => "DownloadInProgress"; - public ulong BytesReceived { get; private set; } + public ByteTransferProgress ProgressObject { get; private set; } - public ulong TotalBytesToReceive { get; private set; } - - public DownloadOperationReport(ulong bytesReceived, ulong totalBytesToReceive) + public DownloadOperationReport(ByteTransferProgress progressObj) { - BytesReceived = bytesReceived; - TotalBytesToReceive = totalBytesToReceive; + ProgressObject = progressObj; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs index bf6bd3eb1..e418d3266 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/IOperationReport.cs @@ -15,7 +15,5 @@ public interface IOperationReport public string LocalizationKey { get; } - public ulong BytesReceived { get; } - - public ulong TotalBytesToReceive { get; } + public ByteTransferProgress ProgressObject { get; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs index 51d52cba2..73ecb39d1 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryCreationUserInput.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json.Serialization; + namespace HyperVExtension.Models.VirtualMachineCreation; /// <summary> @@ -8,7 +10,8 @@ namespace HyperVExtension.Models.VirtualMachineCreation; /// </summary> public sealed class VMGalleryCreationUserInput { - public string NewVirtualMachineName { get; set; } = string.Empty; + public string NewEnvironmentName { get; set; } = string.Empty; + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public int SelectedImageListIndex { get; set; } } diff --git a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs index 64463af6d..8bf3fcb3b 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VirtualMachineCreation/VMGalleryVMCreationOperation.cs @@ -79,14 +79,7 @@ public VMGalleryVMCreationOperation( /// <param name="value">The archive extraction operation returned by the progress handler which extracts the archive file</param> public void Report(IOperationReport value) { - var displayText = Image.Name; - - if (value.ReportKind == ReportKind.ArchiveExtraction) - { - displayText = $"{ArchivedFile!.Name} ({Image.Name})"; - } - - UpdateProgress(value, value.LocalizationKey, displayText); + UpdateProgress(value, value.LocalizationKey, $"({Image.Name})"); } /// <summary> @@ -114,6 +107,7 @@ public void Report(IOperationReport value) IsOperationInProgress = true; } + UpdateProgress(_stringResource.GetLocalized("CreationStarting", $"({_userInputParameters.NewEnvironmentName})")); var imageList = await _vmGalleryService.GetGalleryImagesAsync(); if (imageList.Images.Count == 0) { @@ -130,12 +124,12 @@ public void Report(IOperationReport value) var archiveProvider = _archiveProviderFactory.CreateArchiveProvider(ArchivedFile!.FileType); await archiveProvider.ExtractArchiveAsync(this, ArchivedFile!, absoluteFilePathForVhd, CancellationTokenSource.Token); - var virtualMachineName = MakeFileNameValid(_userInputParameters.NewVirtualMachineName); + var virtualMachineName = MakeFileNameValid(_userInputParameters.NewEnvironmentName); // Use the Hyper-V manager to create the VM. UpdateProgress(_stringResource.GetLocalized("CreationInProgress", virtualMachineName)); var creationParameters = new VirtualMachineCreationParameters( - _userInputParameters.NewVirtualMachineName, + _userInputParameters.NewEnvironmentName, GetVirtualMachineProcessorCount(), absoluteFilePathForVhd, Image.Config.SecureBoot, @@ -145,7 +139,7 @@ public void Report(IOperationReport value) } catch (Exception ex) { - _log.Error("Operation to create compute system failed", ex); + _log.Error(ex, "Operation to create compute system failed"); ComputeSystemResult = new CreateComputeSystemResult(ex, ex.Message, ex.Message); } @@ -158,16 +152,29 @@ public void Report(IOperationReport value) private void UpdateProgress(IOperationReport report, string localizedKey, string fileName) { - var bytesReceivedSoFar = BytesHelper.ConvertBytesToString(report.BytesReceived); - var totalBytesToReceive = BytesHelper.ConvertBytesToString(report.TotalBytesToReceive); - var progressPercentage = (uint)((report.BytesReceived / (double)report.TotalBytesToReceive) * 100D); - var displayString = _stringResource.GetLocalized(localizedKey, fileName, $"{bytesReceivedSoFar}/{totalBytesToReceive}"); - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(displayString, progressPercentage)); + var bytesReceivedSoFar = BytesHelper.ConvertBytesToString((ulong)report.ProgressObject.BytesReceived); + var totalBytesToReceive = BytesHelper.ConvertBytesToString((ulong)report.ProgressObject.TotalBytesToReceive); + var displayString = _stringResource.GetLocalized(localizedKey, fileName, $"{bytesReceivedSoFar} / {totalBytesToReceive}"); + try + { + Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(displayString, report.ProgressObject.PercentageComplete)); + } + catch (Exception ex) + { + _log.Error(ex, "Failed to update progress"); + } } private void UpdateProgress(string localizedString, uint percentage = 0u) { - Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(localizedString, percentage)); + try + { + Progress?.Invoke(this, new CreateComputeSystemProgressEventArgs(localizedString, percentage)); + } + catch (Exception ex) + { + _log.Error(ex, "Failed to update progress"); + } } /// <summary> @@ -214,7 +221,7 @@ private async Task DeleteFileIfExists(StorageFile file) } catch (Exception ex) { - _log.Error($"Failed to delete file {file.Path}", ex); + _log.Error(ex, $"Failed to delete file {file.Path}"); } } @@ -227,7 +234,7 @@ private string MakeFileNameValid(string originalName) private string GetUniqueAbsoluteFilePath(string defaultVirtualDiskPath) { var extension = Path.GetExtension(Image.Disk.ArchiveRelativePath); - var expectedExtractedFileLocation = Path.Combine(defaultVirtualDiskPath, $"{_userInputParameters.NewVirtualMachineName}{extension}"); + var expectedExtractedFileLocation = Path.Combine(defaultVirtualDiskPath, $"{_userInputParameters.NewEnvironmentName}{extension}"); var appendedNumber = 1u; var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(expectedExtractedFileLocation); diff --git a/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs index 009dc78c9..b7be3a7ef 100644 --- a/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs +++ b/HyperVExtension/src/HyperVExtension/Models/VmCredentialAdaptiveCardSession.cs @@ -163,7 +163,7 @@ public IAsyncOperation<ProviderOperationResult> OnAction(string action, string i } catch (Exception ex) { - _log.Error($"Exception in OnAction: {ex}"); + _log.Error(ex, $"Exception in OnAction: {ex}"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Something went wrong", ex.Message); } diff --git a/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs b/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs index 677aa14d1..c5baabb0f 100644 --- a/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs +++ b/HyperVExtension/src/HyperVExtension/Models/WaitForLoginAdaptiveCardSession.cs @@ -144,7 +144,7 @@ public IAsyncOperation<ProviderOperationResult> OnAction(string action, string i } catch (Exception ex) { - _log.Error($"Exception in OnAction: {ex}"); + _log.Error(ex, $"Exception in OnAction: {ex}"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Something went wrong", ex.Message); } diff --git a/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs b/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs index c625cd657..dd00cd537 100644 --- a/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs +++ b/HyperVExtension/src/HyperVExtension/Providers/HyperVProvider.cs @@ -4,6 +4,7 @@ using System.Text.Json; using HyperVExtension.Common; using HyperVExtension.Helpers; +using HyperVExtension.Models; using HyperVExtension.Models.VirtualMachineCreation; using HyperVExtension.Services; using Microsoft.Windows.DevHome.SDK; @@ -25,14 +26,21 @@ public class HyperVProvider : IComputeSystemProvider private readonly VmGalleryCreationOperationFactory _vmGalleryCreationOperationFactory; + private readonly IVMGalleryService _vmGalleryService; + // Temporary will need to add more error strings for different operations. public string OperationErrorString => _stringResource.GetLocalized(errorResourceKey); - public HyperVProvider(IHyperVManager hyperVManager, IStringResource stringResource, VmGalleryCreationOperationFactory vmGalleryCreationOperationFactory) + public HyperVProvider( + IHyperVManager hyperVManager, + IStringResource stringResource, + VmGalleryCreationOperationFactory vmGalleryCreationOperationFactory, + IVMGalleryService vmGalleryService) { _hyperVManager = hyperVManager; _stringResource = stringResource; _vmGalleryCreationOperationFactory = vmGalleryCreationOperationFactory; + _vmGalleryService = vmGalleryService; } /// <summary> Gets or sets the default compute system properties. </summary> @@ -67,7 +75,7 @@ public IAsyncOperation<ComputeSystemsResult> GetComputeSystemsAsync(IDeveloperId } catch (Exception ex) { - _log.Error($"Failed to retrieved all virtual machines on: {DateTime.Now}", ex); + _log.Error(ex, $"Failed to retrieved all virtual machines on: {DateTime.Now}"); return new ComputeSystemsResult(ex, OperationErrorString, ex.Message); } }).AsAsyncOperation(); @@ -75,9 +83,8 @@ public IAsyncOperation<ComputeSystemsResult> GetComputeSystemsAsync(IDeveloperId public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForDeveloperId(IDeveloperId developerId, ComputeSystemAdaptiveCardKind sessionKind) { - // This won't be supported until creation is supported. - var notImplementedException = new NotImplementedException($"Method not implemented by Hyper-V Compute System Provider"); - return new ComputeSystemAdaptiveCardResult(notImplementedException, OperationErrorString, notImplementedException.Message); + var imageList = _vmGalleryService.GetGalleryImagesAsync().GetAwaiter().GetResult(); + return new ComputeSystemAdaptiveCardResult(new VMGalleryCreationAdaptiveCardSession(imageList, _stringResource)); } public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem(IComputeSystem computeSystem, ComputeSystemAdaptiveCardKind sessionKind) @@ -98,7 +105,7 @@ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem } catch (Exception ex) { - _log.Error($"Failed to create a new virtual machine on: {DateTime.Now}", ex); + _log.Error(ex, $"Failed to create a new virtual machine on: {DateTime.Now}"); // Dev Home will handle null values as failed operations. We can't throw because this is an out of proc // COM call, so we'll lose the error information. We'll log the error and return null. diff --git a/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 b/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 new file mode 100644 index 000000000..f81bce1a5 --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Scripts/DevSetupAgent.ps1 @@ -0,0 +1,208 @@ +<# +.SYNOPSIS + +Install DevSetupAgent Windows service and DevSetupEngine COM server on a VM + +.DESCRIPTION + +Install DevSetupAgent Windows service and DevSetupEngine COM server on a VM +through the provided PSSession. +#> + +function Install-DevSetupAgent +{ + Param( + [Parameter(Mandatory = $true)] + [Guid] $VMId, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.Runspaces.PSSession] $Session, + + [Parameter(Mandatory = $true)] + [string] $Path + ) + + $ErrorActionPreference = "Stop" + $activity = "Installing DevSetupAgent to VM $VMId" + + # Validate input. Only .cab and .zip files are supported + # If $Path is a directory, it will be copied to the VM and installed as is + $isDirectory = $false + $isCab = $false + $inputFileName = $null + if (Test-Path -Path $Path -PathType 'Container') + { + $isDirectory = $true + } + elseif (Test-Path -Path $Path -PathType 'Leaf') + { + if ($Path -match '\.(cab)$') + { + $isCab = $true + } + elseif (-not $Path -match '\.(zip)$') + { + throw "Only .cab and .zip files are supported" + } + $inputFileName = Split-Path -Path $Path -Leaf + } + else + { + throw "$Path does not exist" + } + + + $DevSetupAgentConst = "DevSetupAgent" + $DevSetupEngineConst = "DevSetupEngine" + $session = $Session + + $guestTempDirectory = Invoke-Command -Session $session -ScriptBlock { $env:temp } + + [string] $guid = [System.Guid]::NewGuid() + $guestUnpackDirectory = Join-Path -Path $guestTempDirectory -ChildPath $guid + $guestDevSetupAgentTempDirectory = Join-Path -Path $guestUnpackDirectory -ChildPath $DevSetupAgentConst + + Write-Host "Creating VM temporary folder $guestUnpackDirectory" + Write-Progress -Activity $activity -Status "Creating VM temporary folder $guestUnpackDirectory" -PercentComplete 10 + Invoke-Command -Session $session -ScriptBlock { New-Item -Path "$using:guestUnpackDirectory" -ItemType "directory" } + + if ($isDirectory) + { + $destinationPath = $guestDevSetupAgentTempDirectory + } + else + { + $destinationPath = $guestUnpackDirectory + } + + Write-Host "Copying $Path to VM $destinationPath" + Write-Progress -Activity $activity -Status "Copying DevSetupAgent to VM $destinationPath" -PercentComplete 15 + Copy-Item -ToSession $session -Recurse -Path $Path -Destination $destinationPath + + + Invoke-Command -Session $session -ScriptBlock { + $ErrorActionPreference = "Stop" + + try + { + $guestDevSetupAgentPath = Join-Path -Path $Env:Programfiles -ChildPath $using:DevSetupAgentConst + + # Stop and remove previous version of DevSetupAgent service if it exists + $service = Get-Service -Name $using:DevSetupAgentConst -ErrorAction SilentlyContinue + if ($service) + { + $serviceWMI = Get-WmiObject -Class Win32_Service -Filter "Name='$using:DevSetupAgentConst'" + $existingServicePath = $serviceWMI.Properties["PathName"].Value + if ($existingServicePath) + { + $guestDevSetupAgentPath = Split-Path $existingServicePath -Parent + } + + try + { + Write-Host "Stopping DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Stopping DevSetupAgent service $destinationPath" -PercentComplete 30 + $service.Stop() + } + catch + { + Write-Host "Ignoring error: $PSItem" + } + + Remove-Variable -Name service -ErrorAction SilentlyContinue + + # Remove-Service is only available in PowerShell 6.0 and later. Windows doesn't come with it preinstalled. + Write-Host "Removing DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Removing DevSetupAgent service" -PercentComplete 35 + $serviceWMI = Get-WmiObject -Class Win32_Service -Filter "Name='$using:DevSetupAgentConst'" + $serviceWMI.Delete() + Remove-Variable -Name serviceWMI -ErrorAction SilentlyContinue + } + + # Stop previous version of DevSetupEngine COM server if it exists + $devSetupEngineProcess = Get-Process -Name "$using:DevSetupEngineConst" -ErrorAction SilentlyContinue + if ($devSetupEngineProcess -ne $null) + { + Write-Host "Stopping $using:DevSetupEngineConst process" + Write-Progress -Activity $using:activity -Status "Stopping $using:DevSetupEngineConst process" -PercentComplete 40 + Stop-Process -Force -Name "$using:DevSetupEngineConst" + } + + # Unregister DevSetupEngine + $enginePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath "$using:DevSetupEngineConst.exe" + if (Test-Path -Path $enginePath) + { + Write-Host "Unregistering DevSetupEngine ($enginePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupEngine ($enginePath)" -PercentComplete 88 + &$enginePath "-UnregisterComServer" + } + + # Remove previous version of DevSetupAgent service files + if (Test-Path -Path $guestDevSetupAgentPath) + { + # Sleep a few seconds to make sure all handles released after shutting down previous DevSetupEngine + Start-Sleep -Seconds 7 + Write-Host "Deleting old DevSetupAgent service files" + Write-Progress -Activity $using:activity -Status "Deleting old DevSetupAgent service files" -PercentComplete 45 + Remove-Item -Recurse -Force -Path $guestDevSetupAgentPath + } + + if ($using:isDirectory) + { + Write-Host "Copying DevSetupAgent to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Deleting old DevSetupAgent service files" -PercentComplete 50 + Copy-Item -Recurse -Path $using:guestDevSetupAgentTempDirectory -Destination $guestDevSetupAgentPath + } + elseif ($using:isCab) + { + $cabPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName + Write-Host "Unpacking $cabPath to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Unpacking $cabPath to $guestDevSetupAgentPath" -PercentComplete 60 + $expandOutput=&"$Env:SystemRoot\System32\expand.exe" $cabPath /F:* $Env:Programfiles + if ($LastExitCode -ne 0) + { + throw "Error unpacking $cabPath`:`n$LastExitCode`n$($expandOutput|Out-String)" + } + } + else + { + $zipPath = Join-Path -Path $using:guestUnpackDirectory -ChildPath $using:inputFileName + Write-Host "Unpacking $using:inputFileName to $guestDevSetupAgentPath" + Write-Progress -Activity $using:activity -Status "Unpacking $using:inputFileName to $guestDevSetupAgentPath" -PercentComplete 60 + Expand-Archive -Path $zipPath -Destination $guestDevSetupAgentPath + } + + # Register DevSetupAgent service + $servicePath = Join-Path -Path $guestDevSetupAgentPath -ChildPath "$using:DevSetupAgentConst.exe" + Write-Host "Registering DevSetupAgent service ($servicePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupAgent service ($servicePath)" -PercentComplete 85 + New-Service -Name $using:DevSetupAgentConst -BinaryPathName $servicePath -StartupType Automatic + + # Register DevSetupEngine + Write-Host "Registering DevSetupEngine ($enginePath)" + Write-Progress -Activity $using:activity -Status "Registering DevSetupEngine ($enginePath)" -PercentComplete 88 + + # Executing non-console apps using '&' does not set $LastExitCode. Using Start-Process here to get the returned error code. + $process = Start-Process -NoNewWindow -Wait $enginePath -ArgumentList "-RegisterComServer" -PassThru + if ($process.ExitCode -ne 0) + { + throw "Error registering $enginePath`: $process.ExitCode" + } + + Write-Host "Starting DevSetupAgent service" + Write-Progress -Activity $using:activity -Status "Starting DevSetupAgent service" -PercentComplete 92 + Start-Service $using:DevSetupAgentConst + } + catch + { + Write-Host "Error on guest OS: $PSItem" + } + finally + { + Write-Host "Removing temporary directory $using:guestUnpackDirectory" + Remove-Item -Recurse -Force -Path $using:guestUnpackDirectory -ErrorAction SilentlyContinue + } + } + + Remove-PSSession $session +} diff --git a/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs b/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs index 9a86fec52..dda443863 100644 --- a/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/DownloaderService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using HyperVExtension.Extensions; +using HyperVExtension.Models; using HyperVExtension.Models.VirtualMachineCreation; namespace HyperVExtension.Services; @@ -32,13 +33,12 @@ public async Task StartDownloadAsync(IProgress<IOperationReport> progressProvide using var outputFileStream = File.OpenWrite(destinationFile); outputFileStream.SetLength(totalBytesToReceive); - var downloadProgress = new Progress<long>(bytesCopied => + var downloadProgress = new Progress<ByteTransferProgress>(progressObj => { - var percentage = (uint)(bytesCopied / (double)totalBytesToReceive * 100D); - progressProvider.Report(new DownloadOperationReport((ulong)bytesCopied, (ulong)totalBytesToReceive)); + progressProvider.Report(new DownloadOperationReport(progressObj)); }); - await webFileStream.CopyToAsync(outputFileStream, downloadProgress, _transferBufferSize, cancellationToken); + await webFileStream.CopyToAsync(outputFileStream, downloadProgress, _transferBufferSize, totalBytesToReceive, cancellationToken); } /// <inheritdoc cref="IDownloaderService.DownloadStringAsync"/> @@ -55,6 +55,12 @@ public async Task<byte[]> DownloadByteArrayAsync(string sourceWebUri, Cancellati return await httpClient.GetByteArrayAsync(sourceWebUri, cancellationToken); } + public async Task<long> GetHeaderContentLength(Uri sourceWebUri, CancellationToken cancellationToken) + { + var httpClient = _httpClientFactory.CreateClient(); + return GetTotalBytesToReceive(await httpClient.GetAsync(sourceWebUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)); + } + private long GetTotalBytesToReceive(HttpResponseMessage response) { if (response.Content.Headers.ContentLength.HasValue) diff --git a/HyperVExtension/src/HyperVExtension/Services/IDownloaderService.cs b/HyperVExtension/src/HyperVExtension/Services/IDownloaderService.cs index 55704fe97..2d4e98226 100644 --- a/HyperVExtension/src/HyperVExtension/Services/IDownloaderService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/IDownloaderService.cs @@ -35,4 +35,6 @@ public interface IDownloaderService /// <param name="cancellationToken">A token that can allow the operation to be cancelled while it is running</param> /// <returns>Content returned by web server represented as an array of bytes</returns> public Task<byte[]> DownloadByteArrayAsync(string sourceWebUri, CancellationToken cancellationToken); + + public Task<long> GetHeaderContentLength(Uri sourceWebUri, CancellationToken cancellationToken); } diff --git a/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs b/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs index dbfb89609..b665df70a 100644 --- a/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/PowerShellService.cs @@ -60,7 +60,7 @@ public PowerShellResult Execute(IEnumerable<PowerShellCommandlineStatement> comm catch (Exception ex) { var commandStrings = string.Join(Environment.NewLine, commandLineStatements.Select(cmd => cmd.ToString())); - _log.Error($"Error running PowerShell commands: {commandStrings}", ex); + _log.Error(ex, $"Error running PowerShell commands: {commandStrings}"); throw; } } diff --git a/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs b/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs index 2c87c051f..e09837ce4 100644 --- a/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs +++ b/HyperVExtension/src/HyperVExtension/Services/VMGalleryService.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Globalization; using System.Security.Cryptography; using System.Text.Json; using HyperVExtension.Models.VMGalleryJsonToClasses; @@ -73,11 +74,20 @@ public async Task<VMGalleryImageList> GetGalleryImagesAsync() image.Symbol.Base64Image = Convert.ToBase64String(byteArray); } + + if (!string.IsNullOrEmpty(image.Disk.Uri)) + { + var totalSizeOfDisk = await _downloaderService.GetHeaderContentLength(new Uri(image.Disk.Uri), cancellationTokenSource.Token); + if (ulong.TryParse(image.Requirements.DiskSpace, CultureInfo.InvariantCulture, out var requiredDiskSpace)) + { + image.Disk.SizeInBytes = (ulong)totalSizeOfDisk; + } + } } } catch (Exception ex) { - _log.Error($"Unable to retrieve VM gallery images", ex); + _log.Error(ex, $"Unable to retrieve VM gallery images"); } return _imageList; diff --git a/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw b/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw index 5586a3455..7f7aa770a 100644 --- a/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw +++ b/HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw @@ -118,8 +118,12 @@ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="CreationInProgress" xml:space="preserve"> - <value>Creating: {0}</value> - <comment>Locked="{0}" text to tell the user that we're currently creating the virtual machine. {0} is the name of the virtual machine</comment> + <value>Adding network switch, secure boot and enhanced session configuration for {0}</value> + <comment>Locked="{0}" Text to tell the user that we're performing post creation actions like adding a network switch to the virtual machine. {0} is the name of the virtual machine</comment> + </data> + <data name="CreationStarting" xml:space="preserve"> + <value>Starting the creation process for {0}</value> + <comment>Locked="{0}" Text to tell the user that we're starting the process to create the virtual machine. {0} is the name of the virtual machine</comment> </data> <data name="CurrentCheckpoint" xml:space="preserve"> <value>Current Checkpoint</value> @@ -130,7 +134,7 @@ <comment>Locked="{0}" text to tell the user that a file exists and we do not need to download it again. {0} is a previously download file. We show the file name in {0}.</comment> </data> <data name="DownloadInProgress" xml:space="preserve"> - <value>Downloading {0}. {1}</value> + <value>Downloading {0} {1}</value> <comment>Locked="{0}" text to tell the user that we are downloading a file from the web. {0} is the file we're downloading. {1} the progress in the form of "bytes received / total bytes needed". E.g "10 Mb / 400 Mb"</comment> </data> <data name="DownloadOperationCancelled" xml:space="preserve"> @@ -158,7 +162,7 @@ <comment>Attempt counter text for the dialog to enter Hyper-V VM admin credential ({CurrentAttempt}/{MaxAttempts}).</comment> </data> <data name="ExtractionInProgress" xml:space="preserve"> - <value>Extracting file {0}. {1}</value> + <value>Extracting file {0} {1}</value> <comment>Locked="{0}" text to tell the user that we're extracting a zip file into a location on their computer. {0} is the zip file we're extracting. {1} the progress in the form of "bytes extracted / total bytes needed". E.g "10 Mb / 400 Mb"</comment> </data> <data name="NoImagesFoundError" xml:space="preserve"> @@ -257,4 +261,72 @@ <value>Please log on to your Hyper-V</value> <comment>Title text of the dialog asking to log in to Hyper-V VM.</comment> </data> + <data name="AdaptiveCardStateNotRecognizedError" xml:space="preserve"> + <value>Adaptive card state not recognized</value> + <comment>Error text to show when we don't recognize the state of the adaptive card that was given to us</comment> + </data> + <data name="AdaptiveCardUnRecognizedAction" xml:space="preserve"> + <value>Action passed to the extension was not recognized. View the extension logs for more information</value> + <comment>Error text to show when we don't recognize the adaptive card action that was passed to the extension</comment> + </data> + <data name="ButtonToLaunchContentDialogLabel" xml:space="preserve"> + <value>More Info</value> + <comment>Text for a button that will launch a content dialog that displays more information to the user about a disk image</comment> + </data> + <data name="DownloadForContentDialog" xml:space="preserve"> + <value>Download</value> + <comment>label text for the download size of the disk image</comment> + </data> + <data name="EnterNewVMNameLabel" xml:space="preserve"> + <value>New virtual machine name</value> + <comment>Label text for textbox where users will enter the name for their new virtual machine</comment> + </data> + <data name="EnterNewVMNamePlaceHolder" xml:space="preserve"> + <value>Enter the name of your new virtual machine</value> + <comment>place holder text that will appear within a text box</comment> + </data> + <data name="InitialCreationFormGenerationFailedError" xml:space="preserve"> + <value>Failed to generate the initial creation form</value> + <comment>Error text to show the user when the was an error getting the initial disk image selection page in the creation wizard flow</comment> + </data> + <data name="LastUpdatedForContentDialog" xml:space="preserve"> + <value>Last updated</value> + <comment>label text for when the disk image was last updated</comment> + </data> + <data name="LocaleForContentDialog" xml:space="preserve"> + <value>Locale</value> + <comment>label text for locale of operation system that is installed on the disk image</comment> + </data> + <data name="NameLabelForNewVirtualMachine" xml:space="preserve"> + <value>Name{0}</value> + <comment>Locked="{0}" text label that will be on top of the name the user provides in a textbox. {0} is the colon e.g ":" special character</comment> + </data> + <data name="OsVersionForContentDialog" xml:space="preserve"> + <value>Version</value> + <comment>label text for version of operating system that is installed on the disk image</comment> + </data> + <data name="PrimaryButtonForContentDialogText" xml:space="preserve"> + <value>Ok</value> + <comment>Text for primary button of the content dialog </comment> + </data> + <data name="PrimaryButtonLabelForCreationFlow" xml:space="preserve"> + <value>Next</value> + <comment>Text to display to the user about what the primary button does in the UI</comment> + </data> + <data name="ReviewFormGenerationFailedError" xml:space="preserve"> + <value>Failed to generate the review form</value> + <comment>Error text to show the user when the was an error getting the review page in the creation wizard flow</comment> + </data> + <data name="SecondaryButtonForContentDialogText" xml:space="preserve"> + <value>Cancel</value> + <comment>Text for the secondary button of the content dialog</comment> + </data> + <data name="SecondaryButtonLabelForCreationFlow" xml:space="preserve"> + <value>Previous</value> + <comment>Text to display to the user about what the secondary button does in the UI.</comment> + </data> + <data name="SettingsCardLabel" xml:space="preserve"> + <value>Choose an image to use</value> + <comment>Label text for a list of cards that appear in the UI</comment> + </data> </root> \ No newline at end of file diff --git a/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json b/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json new file mode 100644 index 000000000..31a9d10b4 --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Templates/InitialVMGalleryCreationForm.json @@ -0,0 +1,85 @@ +{ + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.5", + "body": [ + { + "type": "Input.Text", + "id": "NewEnvironmentName", + "label": "${EnterNewVMNameLabel}", + "placeholder": "${EnterNewVMNamePlaceHolder}", + "maxLength": 100, + "isRequired": true, + "Spacing": "Large" + }, + { + "type": "DevHome.SettingsCardChoiceSet", + "id": "SelectedImageListIndex", + "label": "${SettingsCardLabel}", + "isRequired": true, + "devHomeSettingsCards": [ + { + "type": "DevHome.SettingsCard", + "$data": "${GalleryImages}", + "devHomeSettingsCardDescription": "${SubDescription}", + "devHomeSettingsCardHeader": "${Header}", + "devHomeSettingsCardHeaderIcon": "${HeaderIcon}", + "devHomeSettingsCardActionElement": { + "type": "DevHome.LaunchContentDialogButton", + "devHomeActionText": "${ButtonToLaunchContentDialogLabel}", + "devHomeContentDialogContent": { + "devHomeContentDialogTitle": "${Header}", + "devHomeContentDialogBodyAdaptiveCard": { + "type": "AdaptiveCard", + "version": "1.5", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "body": [ + { + "type": "Container", + "$data": "${ContentDialogInfo}", + "items": [ + { + "type": "TextBlock", + "text": "${ImageDescription}", + "isMultiline": true, + "Spacing": "Medium", + "size": "Medium", + "wrap": true + }, + { + "$data": "${GalleryImageFacts}", + "type": "FactSet", + "facts": [ + { + "title": "${title}", + "value": "${value}" + } + ] + } + ] + } + ] + }, + "devHomeContentDialogSecondaryButtonText": "${SecondaryButtonForContentDialogText}" + } + } + } + ] + }, + { + "type": "ActionSet", + "actions": [ + { + "id": "DevHomeMachineConfigurationNextButton", + "type": "Action.Submit", + "title": "${PrimaryButtonLabelForCreationFlow}" + }, + { + "id": "DevHomeMachineConfigurationPreviousButton", + "type": "Action.Submit", + "title": "${SecondaryButtonLabelForCreationFlow}" + } + ] + } + ] +} \ No newline at end of file diff --git a/HyperVExtension/src/HyperVExtension/Templates/ReviewFormForVMGallery.json b/HyperVExtension/src/HyperVExtension/Templates/ReviewFormForVMGallery.json new file mode 100644 index 000000000..20335479b --- /dev/null +++ b/HyperVExtension/src/HyperVExtension/Templates/ReviewFormForVMGallery.json @@ -0,0 +1,98 @@ +{ + "type": "AdaptiveCard", + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "version": "1.6", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "spacing": "medium", + "items": [ + { + "type": "TextBlock", + "text": "${ProviderName}", + "wrap": true, + "size": "medium" + }, + { + "type": "TextBlock", + "text": "${DiskImageSize}", + "wrap": true, + "size": "medium" + } + ] + }, + { + "type": "Column", + "width": "auto", + "spacing": "extraLarge", + "items": [ + { + "type": "TextBlock", + "text": "${NameLabel}", + "wrap": true, + "size": "medium" + }, + { + "type": "TextBlock", + "text": "${NameOfNewVM}", + "wrap": true, + "size": "medium" + } + ] + }, + { + "type": "Column", + "width": "auto", + "spacing": "extraLarge", + "verticalContentAlignment": "center", + "items": [ + { + "type": "Image", + "url": "${DiskImageUrl}", + "height": "32px" + } + ] + }, + { + "type": "Column", + "width": "auto", + "spacing": "medium", + "items": [ + { + "type": "TextBlock", + "text": "${VMGalleryImageName}", + "wrap": true, + "size": "medium" + }, + { + "type": "TextBlock", + "text": "${Publisher}", + "wrap": true, + "isSubtle": true, + "size": "medium" + } + ] + } + ] + }, + { + "type": "ActionSet", + "actions": [ + { + "id": "DevHomeMachineConfigurationNextButton", + "type": "Action.Submit", + "title": "${PrimaryButtonLabelForCreationFlow}" + }, + { + "id": "DevHomeMachineConfigurationPreviousButton", + "type": "Action.Submit", + "title": "${SecondaryButtonLabelForCreationFlow}" + } + ] + } + ] +} \ No newline at end of file diff --git a/HyperVExtension/src/HyperVExtensionServer/HyperVExtensionServer.csproj b/HyperVExtension/src/HyperVExtensionServer/HyperVExtensionServer.csproj index 8e82ab271..3b3bf6315 100644 --- a/HyperVExtension/src/HyperVExtensionServer/HyperVExtensionServer.csproj +++ b/HyperVExtension/src/HyperVExtensionServer/HyperVExtensionServer.csproj @@ -14,9 +14,9 @@ <UseWinUI>false</UseWinUI> <CsWinRTEnabled>false</CsWinRTEnabled> <StartupObject>HyperVExtension.Program</StartupObject> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Platforms>x86;x64;arm64</Platforms> - <PublishProfileFullPath Condition="'$(BuildingInsideVisualStudio)' != 'True'">$(SolutionDir)\src\Properties\PublishProfiles\win10-$(Platform).pubxml</PublishProfileFullPath> + <PublishProfileFullPath Condition="'$(BuildingInsideVisualStudio)' != 'True'">$(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfileFullPath> </PropertyGroup> <ItemGroup> diff --git a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-arm64.pubxml b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-arm64.pubxml similarity index 91% rename from HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-arm64.pubxml rename to HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-arm64.pubxml index 2593bad1c..14e602a69 100644 --- a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-arm64.pubxml +++ b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-arm64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>arm64</Platform> - <RuntimeIdentifier>win10-arm64</RuntimeIdentifier> + <RuntimeIdentifier>win-arm64</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x64.pubxml b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x64.pubxml similarity index 91% rename from HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x64.pubxml rename to HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x64.pubxml index 8b6ea06a1..afc8a98a2 100644 --- a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x64.pubxml +++ b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>x64</Platform> - <RuntimeIdentifier>win10-x64</RuntimeIdentifier> + <RuntimeIdentifier>win-x64</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x86.pubxml b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x86.pubxml similarity index 91% rename from HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x86.pubxml rename to HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x86.pubxml index 99985acad..540839963 100644 --- a/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win10-x86.pubxml +++ b/HyperVExtension/src/HyperVExtensionServer/Properties/PublishProfiles/win-x86.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>x86</Platform> - <RuntimeIdentifier>win10-x86</RuntimeIdentifier> + <RuntimeIdentifier>win-x86</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json b/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json index 15a4f5fad..f79a3cf6d 100644 --- a/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json +++ b/HyperVExtension/src/HyperVExtensionServer/appsettings_hyperv.json @@ -6,7 +6,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -14,7 +14,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\hyperv.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/HyperVExtension/src/Telemetry/HyperVExtension.Telemetry.csproj b/HyperVExtension/src/Telemetry/HyperVExtension.Telemetry.csproj index 467a65659..d096236e4 100644 --- a/HyperVExtension/src/Telemetry/HyperVExtension.Telemetry.csproj +++ b/HyperVExtension/src/Telemetry/HyperVExtension.Telemetry.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>HyperVExtension.Telemetry</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <UseWinUI>true</UseWinUI> </PropertyGroup> <ItemGroup> diff --git a/HyperVExtension/test/DevSetupAgent.Test/DevSetupAgent.Test.csproj b/HyperVExtension/test/DevSetupAgent.Test/DevSetupAgent.Test.csproj index 372bd9bd6..d0d2d2c48 100644 --- a/HyperVExtension/test/DevSetupAgent.Test/DevSetupAgent.Test.csproj +++ b/HyperVExtension/test/DevSetupAgent.Test/DevSetupAgent.Test.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Platforms>x86;x64;arm64</Platforms> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> diff --git a/HyperVExtension/test/DevSetupEngine.Test/DevSetupEngine.Test.csproj b/HyperVExtension/test/DevSetupEngine.Test/DevSetupEngine.Test.csproj index 0378daeb3..768c60e6a 100644 --- a/HyperVExtension/test/DevSetupEngine.Test/DevSetupEngine.Test.csproj +++ b/HyperVExtension/test/DevSetupEngine.Test/DevSetupEngine.Test.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Platforms>x86;x64;arm64</Platforms> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtension.UnitTest.csproj b/HyperVExtension/test/HyperVExtension/HyperVExtension.UnitTest.csproj index 898887e9c..ab0eddaa1 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtension.UnitTest.csproj +++ b/HyperVExtension/test/HyperVExtension/HyperVExtension.UnitTest.csproj @@ -2,7 +2,7 @@ <Import Project="$(SolutionDir)ToolingVersions.props" /> <PropertyGroup> <RootNamespace>HyperVExtension.UnitTest</RootNamespace> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Platforms>x86;x64;arm64</Platforms> <IsPackable>false</IsPackable> <ImplicitUsings>enable</ImplicitUsings> diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs index 97a0aaf1c..8de473bab 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs +++ b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVExtensionIntegrationTest.cs @@ -393,7 +393,7 @@ public async Task TestVirtualMachineCreationFromVmGallery() var smallestImageIndex = await GetIndexOfImageWithSmallestRequiredSpace(imageList); var inputJson = JsonSerializer.Serialize(new VMGalleryCreationUserInput() { - NewVirtualMachineName = expectedVMName, + NewEnvironmentName = expectedVMName, // Get Image with the smallest size from gallery, we'll use it to create a VM. SelectedImageListIndex = smallestImageIndex, diff --git a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs index 68ca35480..68a62af01 100644 --- a/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs +++ b/HyperVExtension/test/HyperVExtension/HyperVExtensionTests/Services/HyperVProviderTests.cs @@ -77,7 +77,7 @@ public async Task HyperVProvider_Can_Create_VirtualMachine() var hyperVProvider = TestHost!.GetService<IComputeSystemProvider>(); var inputJson = JsonSerializer.Serialize(new VMGalleryCreationUserInput() { - NewVirtualMachineName = _expectedVmName, + NewEnvironmentName = _expectedVmName, SelectedImageListIndex = 0, // Our test gallery image list Json only has one image }); diff --git a/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs b/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs index 06dc15a7a..52c8134a3 100644 --- a/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs +++ b/HyperVExtension/test/HyperVExtension/Mocks/DownloaderServiceMock.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using HyperVExtension.Models; using HyperVExtension.Models.VirtualMachineCreation; using HyperVExtension.Services; using Windows.Storage; @@ -11,9 +12,9 @@ public class DownloaderServiceMock : IDownloaderService { private readonly int _totalIterations = 4; - private readonly ulong _totalBytesToReceive = 1000; + private readonly long _totalBytesToReceive = 1000; - private readonly ulong _bytesReceivedEachIteration = 250; + private readonly long _bytesReceivedEachIteration = 250; private readonly IHttpClientFactory _httpClientFactory; @@ -24,12 +25,12 @@ public DownloaderServiceMock(IHttpClientFactory httpClientFactory) public async Task StartDownloadAsync(IProgress<IOperationReport> progressProvider, Uri sourceWebUri, string destinationFile, CancellationToken cancellationToken) { - var bytesReceivedSoFar = 0UL; + var bytesReceivedSoFar = 0L; for (var i = 0; i < _totalIterations; i++) { await Task.Delay(100, cancellationToken); bytesReceivedSoFar += _bytesReceivedEachIteration; - progressProvider.Report(new DownloadOperationReport(bytesReceivedSoFar, _totalBytesToReceive)); + progressProvider.Report(new DownloadOperationReport(new ByteTransferProgress(bytesReceivedSoFar, _totalBytesToReceive))); } var zipFile = await GetTestZipFileInPackage(); @@ -56,4 +57,10 @@ public async Task<byte[]> DownloadByteArrayAsync(string sourceWebUri, Cancellati var httpClient = _httpClientFactory.CreateClient(); return await httpClient.GetByteArrayAsync(sourceWebUri, cancellationToken); } + + public async Task<long> GetHeaderContentLength(Uri sourceWebUri, CancellationToken cancellationToken) + { + await Task.Delay(1, cancellationToken); + return 100L; + } } diff --git a/common/DevHome.Common.csproj b/common/DevHome.Common.csproj index f9b8d6bf6..6fa639c26 100644 --- a/common/DevHome.Common.csproj +++ b/common/DevHome.Common.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.Common</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Nullable>enable</Nullable> <UseWinUI>true</UseWinUI> <DevHomeSDKVersion Condition="$(DevHomeSDKVersion) == ''">$(DevHomeSDKVersion)</DevHomeSDKVersion> diff --git a/common/Environments/Converters/CardStateColorToBrushConverter.cs b/common/Environments/Converters/CardStateColorToBrushConverter.cs index 0ac1303b9..ca01704c0 100644 --- a/common/Environments/Converters/CardStateColorToBrushConverter.cs +++ b/common/Environments/Converters/CardStateColorToBrushConverter.cs @@ -25,6 +25,7 @@ public object Convert(object value, Type targetType, object parameter, string la CardStateColor.Success => (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"], CardStateColor.Neutral => (SolidColorBrush)Application.Current.Resources["SystemFillColorSolidNeutralBrush"], CardStateColor.Caution => (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"], + CardStateColor.Failure => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], _ => (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"], }; } diff --git a/common/Environments/CustomControls/CardBody.xaml b/common/Environments/CustomControls/CardBody.xaml index 5fa356c1f..5c72231e5 100644 --- a/common/Environments/CustomControls/CardBody.xaml +++ b/common/Environments/CustomControls/CardBody.xaml @@ -7,10 +7,14 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:devEnvConverters="using:DevHome.Common.Environments.Converters" xmlns:controls="using:CommunityToolkit.WinUI.Controls" + xmlns:converters="using:CommunityToolkit.WinUI.Converters" mc:Ignorable="d"> <UserControl.Resources> <devEnvConverters:CardStateColorToBrushConverter x:Key="CardStateColorToBrushConverter"/> <devEnvConverters:CardStateToLocalizedTextConverter x:Key="CardStateToLocalizedTextConverter"/> + <converters:BoolToVisibilityConverter x:Key="NegatedBoolToVisibilityConverter" TrueValue="Collapsed" FalseValue="Visible" /> + <converters:EmptyObjectToObjectConverter x:Key="EmptyObjectToVisibleConverter" NotEmptyValue="Visible" EmptyValue="Collapsed"/> + <converters:EmptyObjectToObjectConverter x:Key="EmptyObjectToCollapsedConverter" NotEmptyValue="Collapsed" EmptyValue="Visible"/> </UserControl.Resources> <Grid @@ -24,7 +28,7 @@ <Viewbox Grid.Column="0" Style="{StaticResource ComputeSystemImageStyle}"> - <Grid> + <Grid> <!-- Image for card body. --> <Border Style="{StaticResource ComputeSystemImageBorderStyle}"> <Image @@ -43,21 +47,21 @@ Grid.Column="1" Grid.ColumnSpan="2" Style="{StaticResource CardBodyStackPanelStyle}"> - <StackPanel Orientation="Horizontal"> - <TextBlock - Style="{StaticResource CardBodyStackPanelTextBlockStyle}" - Text="{x:Bind ComputeSystemTitle, Mode=OneWay}" - IsTextSelectionEnabled="True"/> - <TextBlock - Style="{StaticResource CardBodyStackPanelAltTextBlockStyle}" - Text="{x:Bind ComputeSystemAlternativeTitle, Mode=OneWay}" - IsTextSelectionEnabled="True"/> - </StackPanel> + <StackPanel Orientation="Horizontal"> + <TextBlock + Style="{StaticResource CardBodyStackPanelTextBlockStyle}" + Text="{x:Bind ComputeSystemTitle, Mode=OneWay}" + IsTextSelectionEnabled="True"/> + <TextBlock + Style="{StaticResource CardBodyStackPanelAltTextBlockStyle}" + Text="{x:Bind ComputeSystemAlternativeTitle, Mode=OneWay}" + IsTextSelectionEnabled="True"/> + </StackPanel> <Grid ColumnSpacing="{StaticResource ContainerElementSpacing}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> - <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <!-- Colored dot that indicates the visual state of the compute system. --> <Ellipse @@ -67,12 +71,22 @@ <!-- String that indicates the internal state of the compute system. --> <TextBlock Grid.Column="1" + Visibility="{x:Bind ComputeSystemCreationStatus, Mode=OneWay, Converter={StaticResource EmptyObjectToCollapsedConverter}}" Text="{x:Bind CardState, Converter={StaticResource CardStateToLocalizedTextConverter}, Mode=OneWay}" Style="{StaticResource CardBodyStateTextBlockStyle}" IsTextSelectionEnabled="True"/> - </Grid> + <TextBlock + Grid.Column="1" + Visibility="{x:Bind ComputeSystemCreationStatus, Mode=OneWay, Converter={StaticResource EmptyObjectToVisibleConverter}}" + Text="{x:Bind ComputeSystemCreationStatus, Mode=OneWay}" + Style="{StaticResource CardBodyStateTextBlockStyle}" + TextWrapping="Wrap" + TextTrimming="CharacterEllipsis" + IsTextSelectionEnabled="True"/> + </Grid> <!-- Wrap panel from community toolkit that will display properties of variable length. --> <ItemsRepeater + Visibility="{x:Bind ShouldShowInDefiniteProgress, Mode=OneWay, Converter={StaticResource NegatedBoolToVisibilityConverter}}" ItemsSource="{x:Bind ComputeSystemProperties, Mode=OneWay}" ItemTemplate="{x:Bind ComputeSystemPropertyTemplate, Mode=OneWay}"> <ItemsRepeater.Layout> @@ -81,6 +95,10 @@ HorizontalSpacing="15"/> </ItemsRepeater.Layout> </ItemsRepeater> + <ProgressBar + Margin="0 5 0 0" + Visibility="{x:Bind ShouldShowInDefiniteProgress, Mode=OneWay}" + IsIndeterminate="True"/> </StackPanel> <!-- For an action control, like a splitbutton or checkbox. Pass a Datatemplate to the CardBody's ActionControlTemplate in xaml. @@ -91,5 +109,5 @@ HorizontalContentAlignment="Right" VerticalContentAlignment="Stretch" ContentTemplate="{x:Bind ActionControlTemplate, Mode=OneWay}"/> - </Grid> + </Grid> </UserControl> diff --git a/common/Environments/CustomControls/CardBody.xaml.cs b/common/Environments/CustomControls/CardBody.xaml.cs index 61aacbbee..00de92a48 100644 --- a/common/Environments/CustomControls/CardBody.xaml.cs +++ b/common/Environments/CustomControls/CardBody.xaml.cs @@ -38,6 +38,12 @@ public string ComputeSystemAlternativeTitle set => SetValue(ComputeSystemAlternativeTitleProperty, value); } + public string ComputeSystemCreationStatus + { + get => (string)GetValue(ComputeSystemCreationStatusProperty); + set => SetValue(ComputeSystemCreationStatusProperty, value); + } + public BitmapImage ComputeSystemImage { get => (BitmapImage)GetValue(ComputeSystemImageProperty); @@ -62,6 +68,12 @@ public ObservableCollection<CardProperty> ComputeSystemProperties set => SetValue(ComputeSystemPropertiesProperty, value); } + public bool ShouldShowInDefiniteProgress + { + get => (bool)GetValue(ShouldShowInDefiniteProgressProperty); + set => SetValue(ShouldShowInDefiniteProgressProperty, value); + } + public DataTemplate ComputeSystemPropertyTemplate { get => (DataTemplate)GetValue(ComputeSystemPropertyTemplateProperty); @@ -90,4 +102,8 @@ private static void OnCardBodyChanged(CardBody cardBody, BitmapImage args) private static readonly DependencyProperty ComputeSystemImageProperty = DependencyProperty.Register(nameof(ComputeSystemImage), typeof(BitmapImage), typeof(CardBody), new PropertyMetadata(new BitmapImage { UriSource = new Uri(DefaultCardBodyImagePath) }, (s, e) => OnCardBodyChanged((CardBody)s, (BitmapImage)e.NewValue))); private static readonly DependencyProperty ComputeSystemPropertiesProperty = DependencyProperty.Register(nameof(ComputeSystemProperties), typeof(ObservableCollection<CardProperty>), typeof(CardBody), new PropertyMetadata(null)); private static readonly DependencyProperty ComputeSystemPropertyTemplateProperty = DependencyProperty.Register(nameof(ComputeSystemPropertyTemplate), typeof(DataTemplate), typeof(CardBody), new PropertyMetadata(null)); + + private static readonly DependencyProperty ComputeSystemCreationStatusProperty = DependencyProperty.Register(nameof(ComputeSystemCreationStatus), typeof(string), typeof(CardBody), new PropertyMetadata(null)); + + private static readonly DependencyProperty ShouldShowInDefiniteProgressProperty = DependencyProperty.Register(nameof(ShouldShowInDefiniteProgress), typeof(bool), typeof(CardBody), new PropertyMetadata(false)); } diff --git a/common/Environments/Exceptions/CreateCreateComputeSystemOperationException.cs b/common/Environments/Exceptions/CreateCreateComputeSystemOperationException.cs new file mode 100644 index 000000000..9a2597ccb --- /dev/null +++ b/common/Environments/Exceptions/CreateCreateComputeSystemOperationException.cs @@ -0,0 +1,18 @@ +// 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; + +namespace DevHome.Common.Environments.Exceptions; + +public class CreateCreateComputeSystemOperationException : Exception +{ + public CreateCreateComputeSystemOperationException(string message) + : base(message) + { + } +} diff --git a/common/Environments/Helpers/ComputeSystemHelpers.cs b/common/Environments/Helpers/ComputeSystemHelpers.cs index f97ff096b..779519de5 100644 --- a/common/Environments/Helpers/ComputeSystemHelpers.cs +++ b/common/Environments/Helpers/ComputeSystemHelpers.cs @@ -35,7 +35,7 @@ public static class ComputeSystemHelpers } catch (Exception ex) { - Log.Error($"Failed to get thumbnail for compute system {computeSystemWrapper}.", ex); + Log.Error(ex, $"Failed to get thumbnail for compute system {computeSystemWrapper}."); return null; } } @@ -56,7 +56,7 @@ public static async Task<List<CardProperty>> GetComputeSystemPropertiesAsync(Com } catch (Exception ex) { - Log.Error($"Failed to get all properties for compute system {computeSystemWrapper}.", ex); + Log.Error(ex, $"Failed to get all properties for compute system {computeSystemWrapper}."); return propertyList; } } diff --git a/common/Environments/Helpers/StringResourceHelper.cs b/common/Environments/Helpers/StringResourceHelper.cs index 428153102..3e1982327 100644 --- a/common/Environments/Helpers/StringResourceHelper.cs +++ b/common/Environments/Helpers/StringResourceHelper.cs @@ -26,7 +26,7 @@ public static string GetResource(string key, params object[] args) } catch (Exception ex) { - Log.Error($"Failed to get resource for key {key}.", ex); + Log.Error(ex, $"Failed to get resource for key {key}."); return key; } } diff --git a/common/Environments/Models/CardProperty.cs b/common/Environments/Models/CardProperty.cs index 7323c15d2..2bf9f0c89 100644 --- a/common/Environments/Models/CardProperty.cs +++ b/common/Environments/Models/CardProperty.cs @@ -33,6 +33,7 @@ public enum CardStateColor Success, Neutral, Caution, + Failure, } public partial class CardProperty : ObservableObject @@ -141,7 +142,7 @@ public static unsafe BitmapImage ConvertMsResourceToIcon(Uri iconPathUri, string } catch (Exception ex) { - Log.Error($"Failed to load icon from ms-resource: {iconPathUri} for package: {packageFullName} due to error:", ex); + Log.Error(ex, $"Failed to load icon from ms-resource: {iconPathUri} for package: {packageFullName} due to error:"); } return new BitmapImage(); @@ -199,7 +200,7 @@ public string ConvertBytesToString(object? size) } catch (Exception ex) { - Log.Error($"Failed to convert size in bytes to ulong. Error: {ex}"); + Log.Error(ex, $"Failed to convert size in bytes to ulong. Error: {ex}"); return string.Empty; } } diff --git a/common/Environments/Models/ComputeSystem.cs b/common/Environments/Models/ComputeSystem.cs index 4cdf9d64e..ae21a8007 100644 --- a/common/Environments/Models/ComputeSystem.cs +++ b/common/Environments/Models/ComputeSystem.cs @@ -63,7 +63,7 @@ public void OnComputeSystemStateChanged(object? sender, ComputeSystemState state } catch (Exception ex) { - _log.Error($"OnComputeSystemStateChanged for: {this} failed due to exception", ex); + _log.Error(ex, $"OnComputeSystemStateChanged for: {this} failed due to exception"); } } @@ -75,7 +75,7 @@ public async Task<ComputeSystemStateResult> GetStateAsync() } catch (Exception ex) { - _log.Error($"GetStateAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetStateAsync for: {this} failed due to exception"); return new ComputeSystemStateResult(ex, errorString, ex.Message); } } @@ -88,7 +88,7 @@ public async Task<ComputeSystemOperationResult> StartAsync(string options) } catch (Exception ex) { - _log.Error($"StartAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"StartAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -101,7 +101,7 @@ public async Task<ComputeSystemOperationResult> ShutDownAsync(string options) } catch (Exception ex) { - _log.Error($"ShutDownAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"ShutDownAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -114,7 +114,7 @@ public async Task<ComputeSystemOperationResult> RestartAsync(string options) } catch (Exception ex) { - _log.Error($"RestartAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"RestartAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -127,7 +127,7 @@ public async Task<ComputeSystemOperationResult> TerminateAsync(string options) } catch (Exception ex) { - _log.Error($"TerminateAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"TerminateAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -140,7 +140,7 @@ public async Task<ComputeSystemOperationResult> DeleteAsync(string options) } catch (Exception ex) { - _log.Error($"DeleteAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"DeleteAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -153,7 +153,7 @@ public async Task<ComputeSystemOperationResult> SaveAsync(string options) } catch (Exception ex) { - _log.Error($"SaveAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"SaveAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -166,7 +166,7 @@ public async Task<ComputeSystemOperationResult> PauseAsync(string options) } catch (Exception ex) { - _log.Error($"PauseAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"PauseAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -179,7 +179,7 @@ public async Task<ComputeSystemOperationResult> ResumeAsync(string options) } catch (Exception ex) { - _log.Error($"ResumeAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"ResumeAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -192,7 +192,7 @@ public async Task<ComputeSystemOperationResult> CreateSnapshotAsync(string optio } catch (Exception ex) { - _log.Error($"CreateSnapshotAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"CreateSnapshotAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -205,7 +205,7 @@ public async Task<ComputeSystemOperationResult> RevertSnapshotAsync(string optio } catch (Exception ex) { - _log.Error($"RevertSnapshotAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"RevertSnapshotAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -218,7 +218,7 @@ public async Task<ComputeSystemOperationResult> DeleteSnapshotAsync(string optio } catch (Exception ex) { - _log.Error($"DeleteSnapshotAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"DeleteSnapshotAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -231,7 +231,7 @@ public async Task<ComputeSystemOperationResult> ModifyPropertiesAsync(string opt } catch (Exception ex) { - _log.Error($"ModifyPropertiesAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"ModifyPropertiesAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -244,7 +244,7 @@ public async Task<ComputeSystemThumbnailResult> GetComputeSystemThumbnailAsync(s } catch (Exception ex) { - _log.Error($"GetComputeSystemThumbnailAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetComputeSystemThumbnailAsync for: {this} failed due to exception"); return new ComputeSystemThumbnailResult(ex, errorString, ex.Message); } } @@ -257,7 +257,7 @@ public async Task<IEnumerable<ComputeSystemProperty>> GetComputeSystemProperties } catch (Exception ex) { - _log.Error($"GetComputeSystemPropertiesAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetComputeSystemPropertiesAsync for: {this} failed due to exception"); return new List<ComputeSystemProperty>(); } } @@ -270,7 +270,7 @@ public async Task<ComputeSystemOperationResult> ConnectAsync(string options) } catch (Exception ex) { - _log.Error($"ConnectAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"ConnectAsync for: {this} failed due to exception"); return new ComputeSystemOperationResult(ex, errorString, ex.Message); } } @@ -283,7 +283,7 @@ public IApplyConfigurationOperation ApplyConfiguration(string configuration) } catch (Exception ex) { - _log.Error($"ApplyConfiguration for: {this} failed due to exception", ex); + _log.Error(ex, $"ApplyConfiguration for: {this} failed due to exception"); throw; } } diff --git a/common/Environments/Models/ComputeSystemProvider.cs b/common/Environments/Models/ComputeSystemProvider.cs index 4b8aae984..ad6420324 100644 --- a/common/Environments/Models/ComputeSystemProvider.cs +++ b/common/Environments/Models/ComputeSystemProvider.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Text; using System.Threading.Tasks; +using DevHome.Common.Environments.Exceptions; using DevHome.Common.Environments.Helpers; using DevHome.Common.Helpers; using Microsoft.Windows.DevHome.SDK; @@ -51,12 +52,12 @@ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForDeveloperId(I } catch (Exception ex) { - _log.Error($"CreateAdaptiveCardSessionWithDeveloperId for: {this} failed due to exception", ex); + _log.Error(ex, $"CreateAdaptiveCardSessionForDeveloperId for: {this} failed due to exception"); return new ComputeSystemAdaptiveCardResult(ex, errorString, ex.Message); } } - public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSession(IComputeSystem computeSystem, ComputeSystemAdaptiveCardKind sessionKind) + public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSessionForComputeSystem(IComputeSystem computeSystem, ComputeSystemAdaptiveCardKind sessionKind) { try { @@ -64,7 +65,7 @@ public ComputeSystemAdaptiveCardResult CreateAdaptiveCardSession(IComputeSystem } catch (Exception ex) { - _log.Error($"CreateAdaptiveCardSessionWithComputeSystem for: {this} failed due to exception", ex); + _log.Error(ex, $"CreateAdaptiveCardSessionForComputeSystem for: {this} failed due to exception"); return new ComputeSystemAdaptiveCardResult(ex, errorString, ex.Message); } } @@ -77,11 +78,25 @@ public async Task<ComputeSystemsResult> GetComputeSystemsAsync(IDeveloperId deve } catch (Exception ex) { - _log.Error($"GetComputeSystemsAsync for: {this} failed due to exception", ex); + _log.Error(ex, $"GetComputeSystemsAsync for: {this} failed due to exception"); return new ComputeSystemsResult(ex, errorString, ex.Message); } } + public ICreateComputeSystemOperation? CreateCreateComputeSystemOperation(IDeveloperId developerId, string inputJson) + { + try + { + return _computeSystemProvider.CreateCreateComputeSystemOperation(developerId, inputJson) + ?? throw new CreateCreateComputeSystemOperationException("CreateCreateComputeSystemOperation was null"); + } + catch (Exception ex) + { + _log.Error(ex, $"GetComputeSystemsAsync for: {this} failed due to exception"); + return new FailedCreateComputeSystemOperation(ex, StringResourceHelper.GetResource("CreationOperationStoppedUnexpectedly")); + } + } + public override string ToString() { StringBuilder builder = new(); diff --git a/common/Environments/Models/CreateComputeSystemOperation.cs b/common/Environments/Models/CreateComputeSystemOperation.cs new file mode 100644 index 000000000..ecf69adc0 --- /dev/null +++ b/common/Environments/Models/CreateComputeSystemOperation.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using DevHome.Common.Environments.Helpers; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using Windows.Foundation; + +namespace DevHome.Common.Environments.Models; + +/// <summary> +/// Wrapper class for the out of proc ICreateComputeSystemOperation interface that is received from the extension. +/// </summary> +public class CreateComputeSystemOperation : IDisposable +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CreateComputeSystemOperation)); + + // These operations are stored by the ComputeSystemManager who can then provide them to the environments + // page to be displayed to the user. + public Guid OperationId { get; } = Guid.NewGuid(); + + /// <summary> + /// The original ICreateComputeSystemOperation object that was received from the extension. + /// </summary> + private readonly ICreateComputeSystemOperation _createComputeSystemOperation; + + /// <summary> + /// Wrapper for the <see cref="ICreateComputeSystemOperation.ActionRequired"/> that is received from the extension."/> + /// </summary> + public event TypedEventHandler<CreateComputeSystemOperation, CreateComputeSystemActionRequiredEventArgs>? ActionRequired; + + /// <summary> + /// Wrapper for the <see cref="ICreateComputeSystemOperation.Progress"/> that is received from the extension."/> + /// </summary> + public event TypedEventHandler<CreateComputeSystemOperation, CreateComputeSystemProgressEventArgs>? Progress; + + /// <summary> + /// This is not an extension event, it provides a way for another object receive the result of the operation. + /// </summary> + public event TypedEventHandler<CreateComputeSystemOperation, CreateComputeSystemResult>? Completed; + + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + public string EnvironmentName { get; private set; } = StringResourceHelper.GetResource("EnvironmentGenericName"); + + public ComputeSystemProviderDetails ProviderDetails { get; } + + private readonly Dictionary<string, string> _userInputJsonMap; + + /// <summary> + /// Gets the last progress message that was received from the extension. This is useful if we initialing missed the progress message. + /// </summary> + public string LastProgressMessage { get; private set; } = string.Empty; + + /// <summary> + /// Gets the last progress message that was received from the extension. This is useful if we initialing missed the progress percentage. + /// </summary> + public uint LastProgressPercentage { get; private set; } + + public CreateComputeSystemResult? CreateComputeSystemResult { get; private set; } + + /// <summary> + /// Since we don't actually know what the user input json will look like, we check for the known key name 'NewEnvironmentName'. + /// This "known" key will be provided as guidance to extension creators. However, its really up to the extension to decide what key they want to use. + /// Since this input is coming from an adaptive card from the extension we know we'll only get string key/values pairs. So we expect to see a + /// key/value pair like this: + /// + /// {"NewEnvironmentName" : "MyNewEnvironment"} + /// + /// In the future we can update the ICreateComputeSystemOperation interface in the SDK to have metadata about the new environment. + /// This way we can have strongly typed access to what the new environment name is, as well as other properties we expect an environment to have. + /// </summary> + private const string EnvironmentNameJsonKey = "NewEnvironmentName"; + + private bool _disposedValue; + + public CreateComputeSystemOperation(ICreateComputeSystemOperation createComputeSystemOperation, ComputeSystemProviderDetails providerDetails, string userInputJson) + { + _createComputeSystemOperation = createComputeSystemOperation; + ProviderDetails = providerDetails; + _createComputeSystemOperation.ActionRequired += OnActionRequired; + _createComputeSystemOperation.Progress += OnProgress; + + _userInputJsonMap = JsonSerializer.Deserialize<Dictionary<string, string>>(userInputJson) ?? new(); + + // Try to find the environment name in the user input json. This is the Id of the adaptive card element that allowed the user to enter + // their environment name. If the key is not found, we'll just use the generic name. + foreach (var key in _userInputJsonMap.Keys) + { + if (key.Equals(EnvironmentNameJsonKey, StringComparison.OrdinalIgnoreCase)) + { + EnvironmentName = _userInputJsonMap[key]; + break; + } + } + } + + public void StartOperation() + { + // Fire the task on a background thread so that the UI thread is not blocked. + Task.Run(async () => + { + try + { + CreateComputeSystemResult = await _createComputeSystemOperation.StartAsync().AsTask(_cancellationTokenSource.Token); + Completed?.Invoke(this, CreateComputeSystemResult); + } + catch (Exception ex) + { + _log.Error(ex, $"StartOperation failed for provider {ProviderDetails.ComputeSystemProvider}"); + CreateComputeSystemResult = new CreateComputeSystemResult(ex, StringResourceHelper.GetResource("CreationOperationStoppedUnexpectedly"), ex.Message); + Completed?.Invoke(this, CreateComputeSystemResult); + } + + RemoveEventHandlers(); + }); + } + + private void OnActionRequired(ICreateComputeSystemOperation sender, CreateComputeSystemActionRequiredEventArgs args) + { + ActionRequired?.Invoke(this, args); + } + + private void OnProgress(ICreateComputeSystemOperation sender, CreateComputeSystemProgressEventArgs args) + { + // This object may not appear in th UI, immediately so in case there were progress updates before the UI is ready, we store the last progress message and percentage. + // to be used when the UI is ready. + LastProgressMessage = args.Status; + LastProgressPercentage = args.PercentageCompleted; + + Progress?.Invoke(this, args); + } + + public void RemoveEventHandlers() + { + try + { + _createComputeSystemOperation.ActionRequired -= OnActionRequired; + _createComputeSystemOperation.Progress -= OnProgress; + } + catch (Exception ex) + { + _log.Error(ex, $"Failed to remove event handlers for {this}"); + } + } + + public void CancelOperation() + { + try + { + _cancellationTokenSource.Cancel(); + } + catch (Exception ex) + { + _log.Error(ex, $"Failed to cancel operation for {this}"); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _cancellationTokenSource.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/common/Environments/Models/FailedCreateComputeSystemOperation.cs b/common/Environments/Models/FailedCreateComputeSystemOperation.cs new file mode 100644 index 000000000..da681cb86 --- /dev/null +++ b/common/Environments/Models/FailedCreateComputeSystemOperation.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Windows.DevHome.SDK; +using Windows.Foundation; + +namespace DevHome.Common.Environments.Models; + +/// <summary> +/// Class that represents a failed create compute system operation. We use this when we were unable to get the original +/// ICreateComputeSystemOperation object from the extension. E.g a COM exception was thrown. +/// </summary> +public class FailedCreateComputeSystemOperation : ICreateComputeSystemOperation +{ + public FailedCreateComputeSystemOperation(Exception exception, string localizedErrorMessage) + { + LocalizedErrorMessage = localizedErrorMessage; + Exception = exception; + } + + public string LocalizedErrorMessage { get; } + + public Exception Exception { get; } + + public event TypedEventHandler<ICreateComputeSystemOperation, CreateComputeSystemProgressEventArgs> Progress = (s, e) => { }; + + public event TypedEventHandler<ICreateComputeSystemOperation, CreateComputeSystemActionRequiredEventArgs> ActionRequired = (s, e) => { }; + + public IAsyncOperation<CreateComputeSystemResult> StartAsync() + { + return Task.FromResult(new CreateComputeSystemResult(Exception, LocalizedErrorMessage, Exception.Message)).AsAsyncOperation(); + } +} diff --git a/common/Environments/Services/ComputeSystemManager.cs b/common/Environments/Services/ComputeSystemManager.cs index 2c0f49cac..616566dea 100644 --- a/common/Environments/Services/ComputeSystemManager.cs +++ b/common/Environments/Services/ComputeSystemManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using DevHome.Common.Contracts.Services; @@ -24,8 +25,12 @@ public class ComputeSystemManager : IComputeSystemManager private readonly IComputeSystemService _computeSystemService; + private readonly Dictionary<Guid, CreateComputeSystemOperation> _createComputeSystemOperations = new(); + public event TypedEventHandler<ComputeSystem, ComputeSystemState> ComputeSystemStateChanged = (sender, state) => { }; + private readonly object _creationOperationLock = new(); + // Used in the setup flow to store the ComputeSystem needed to configure. public ComputeSystemReviewItem? ComputeSystemSetupItem { get; set; } @@ -72,17 +77,17 @@ await Parallel.ForEachAsync(computeSystemsProviderDetails, async (providerDetail { if (innerEx is TaskCanceledException) { - _log.Error($"Failed to get retrieve all compute systems from all compute system providers due to cancellation", innerEx); + _log.Error(innerEx, $"Failed to get retrieve all compute systems from all compute system providers due to cancellation"); } else { - _log.Error($"Failed to get retrieve all compute systems from all compute system providers ", innerEx); + _log.Error(innerEx, $"Failed to get retrieve all compute systems from all compute system providers "); } } } catch (Exception ex) { - _log.Error($"Failed to get retrieve all compute systems from all compute system providers ", ex); + _log.Error(ex, $"Failed to get retrieve all compute systems from all compute system providers "); } } @@ -90,4 +95,44 @@ public void OnComputeSystemStateChanged(ComputeSystem sender, ComputeSystemState { ComputeSystemStateChanged(sender, state); } + + public List<CreateComputeSystemOperation> GetRunningOperationsForCreation() + { + lock (_creationOperationLock) + { + return _createComputeSystemOperations.Values.ToList(); + } + } + + public void AddRunningOperationForCreation(CreateComputeSystemOperation operation) + { + lock (_creationOperationLock) + { + _createComputeSystemOperations.Add(operation.OperationId, operation); + } + } + + public void RemoveOperation(CreateComputeSystemOperation operation) + { + lock (_creationOperationLock) + { + _createComputeSystemOperations.Remove(operation.OperationId); + } + } + + public void RemoveAllCompletedOperations() + { + lock (_creationOperationLock) + { + var totalOperations = _createComputeSystemOperations.Count; + for (var i = 0; i < totalOperations; i++) + { + var operation = _createComputeSystemOperations.ElementAt(i).Value; + if (operation.CreateComputeSystemResult != null) + { + _createComputeSystemOperations.Remove(operation.OperationId); + } + } + } + } } diff --git a/common/Environments/Services/IComputeSystemManager.cs b/common/Environments/Services/IComputeSystemManager.cs index 40c6c7c8c..7aba47dc3 100644 --- a/common/Environments/Services/IComputeSystemManager.cs +++ b/common/Environments/Services/IComputeSystemManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading.Tasks; using DevHome.Common.Environments.Models; using Microsoft.Windows.DevHome.SDK; @@ -21,4 +22,12 @@ public interface IComputeSystemManager public event TypedEventHandler<ComputeSystem, ComputeSystemState> ComputeSystemStateChanged; public void OnComputeSystemStateChanged(ComputeSystem sender, ComputeSystemState state); + + public List<CreateComputeSystemOperation> GetRunningOperationsForCreation(); + + public void AddRunningOperationForCreation(CreateComputeSystemOperation operation); + + public void RemoveOperation(CreateComputeSystemOperation operation); + + public void RemoveAllCompletedOperations(); } diff --git a/common/Helpers/AdaptiveCardHelpers.cs b/common/Helpers/AdaptiveCardHelpers.cs index a662b5e7f..2332714e8 100644 --- a/common/Helpers/AdaptiveCardHelpers.cs +++ b/common/Helpers/AdaptiveCardHelpers.cs @@ -32,7 +32,7 @@ public static ImageIcon ConvertBase64StringToImageIcon(string base64String) } catch (Exception ex) { - _log.Error($"Failed to load image icon", ex); + _log.Error(ex, $"Failed to load image icon"); return new ImageIcon(); } } diff --git a/common/Helpers/Deployment.cs b/common/Helpers/Deployment.cs index c62eef22f..c25cb02e3 100644 --- a/common/Helpers/Deployment.cs +++ b/common/Helpers/Deployment.cs @@ -37,7 +37,7 @@ public static Guid Identifier // We do not want this identifer's access to ever create a problem in the // application, so if we can't get it, return empty guid. An empty guid is also a // signal that the data is unknown for filtering purposes. - Log.Error($"Failed getting Deployment Identifier", ex); + Log.Error(ex, $"Failed getting Deployment Identifier"); return Guid.Empty; } } diff --git a/common/Models/ExtensionAdaptiveCardSession.cs b/common/Models/ExtensionAdaptiveCardSession.cs index 9494a1c99..089308e6f 100644 --- a/common/Models/ExtensionAdaptiveCardSession.cs +++ b/common/Models/ExtensionAdaptiveCardSession.cs @@ -21,8 +21,6 @@ public class ExtensionAdaptiveCardSession { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExtensionAdaptiveCardSession)); - private readonly string _componentName = "ExtensionAdaptiveCardSession"; - public IExtensionAdaptiveCardSession Session { get; private set; } public event TypedEventHandler<ExtensionAdaptiveCardSession, ExtensionAdaptiveCardSessionStoppedEventArgs>? Stopped; @@ -45,7 +43,7 @@ public ProviderOperationResult Initialize(IExtensionAdaptiveCard extensionUI) } catch (Exception ex) { - _log.Error(_componentName, $"Initialize failed due to exception", ex); + _log.Error(ex, $"Initialize failed due to exception"); return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message); } } @@ -63,7 +61,7 @@ public void Dispose() } catch (Exception ex) { - _log.Error(_componentName, $"Dispose failed due to exception", ex); + _log.Error(ex, $"Dispose failed due to exception"); } } @@ -75,7 +73,7 @@ public async Task<ProviderOperationResult> OnAction(string action, string inputs } catch (Exception ex) { - _log.Error(_componentName, $"OnAction failed due to exception", ex); + _log.Error(ex, $"OnAction failed due to exception"); return new ProviderOperationResult(ProviderOperationStatus.Failure, ex, ex.Message, ex.Message); } } diff --git a/common/Services/AdaptiveCardRenderingService.cs b/common/Services/AdaptiveCardRenderingService.cs index 19923e01b..3e712f476 100644 --- a/common/Services/AdaptiveCardRenderingService.cs +++ b/common/Services/AdaptiveCardRenderingService.cs @@ -119,7 +119,7 @@ private async Task UpdateHostConfig() } catch (Exception ex) { - _log.Error("Error retrieving HostConfig", ex); + _log.Error(ex, "Error retrieving HostConfig"); } _windowEx.DispatcherQueue.TryEnqueue(() => diff --git a/common/Services/AppInstallManagerService.cs b/common/Services/AppInstallManagerService.cs index a16f63746..4e8e017ef 100644 --- a/common/Services/AppInstallManagerService.cs +++ b/common/Services/AppInstallManagerService.cs @@ -97,7 +97,7 @@ public async Task<bool> TryInstallPackageAsync(string packageId) } catch (Exception ex) { - _log.Error("Package installation Failed", ex); + _log.Error(ex, "Package installation Failed"); } return false; diff --git a/common/Services/ComputeSystemService.cs b/common/Services/ComputeSystemService.cs index dd24c44cc..395b68f1f 100644 --- a/common/Services/ComputeSystemService.cs +++ b/common/Services/ComputeSystemService.cs @@ -83,7 +83,7 @@ public async Task<List<ComputeSystemProviderDetails>> GetComputeSystemProvidersA } catch (Exception ex) { - Log.Error($"Failed to get {nameof(IComputeSystemProvider)} provider from '{extension.Name}'", ex); + Log.Error(ex, $"Failed to get {nameof(IComputeSystemProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'"); } } diff --git a/common/Services/IExtensionWrapper.cs b/common/Services/IExtensionWrapper.cs index 026d36c1d..7a861502c 100644 --- a/common/Services/IExtensionWrapper.cs +++ b/common/Services/IExtensionWrapper.cs @@ -12,68 +12,49 @@ namespace DevHome.Common.Services; public interface IExtensionWrapper { /// <summary> - /// Gets name of the extension as mentioned in the manifest + /// Gets the DisplayName of the package as mentioned in the manifest /// </summary> - string Name - { - get; - } + string PackageDisplayName { get; } + + /// <summary> + /// Gets DisplayName of the extension as mentioned in the manifest + /// </summary> + string ExtensionDisplayName { get; } /// <summary> /// Gets PackageFullName of the extension /// </summary> - string PackageFullName - { - get; - } + string PackageFullName { get; } /// <summary> /// Gets PackageFamilyName of the extension /// </summary> - string PackageFamilyName - { - get; - } + string PackageFamilyName { get; } /// <summary> /// Gets Publisher of the extension /// </summary> - string Publisher - { - get; - } + string Publisher { get; } /// <summary> /// Gets class id (GUID) of the extension class (which implements IExtension) as mentioned in the manifest /// </summary> - string ExtensionClassId - { - get; - } + string ExtensionClassId { get; } /// <summary> /// Gets the date on which the application package was installed or last updated. /// </summary> - DateTimeOffset InstalledDate - { - get; - } + DateTimeOffset InstalledDate { get; } /// <summary> /// Gets the PackageVersion of the extension /// </summary> - PackageVersion Version - { - get; - } + PackageVersion Version { get; } /// <summary> /// Gets the Unique Id for the extension /// </summary> - public string ExtensionUniqueId - { - get; - } + public string ExtensionUniqueId { get; } /// <summary> /// Checks whether we have a reference to the extension process and we are able to call methods on the interface. diff --git a/common/Services/INavigationService.cs b/common/Services/INavigationService.cs index 0a592fc9a..53346e73f 100644 --- a/common/Services/INavigationService.cs +++ b/common/Services/INavigationService.cs @@ -47,4 +47,6 @@ public static class KnownPageKeys public static readonly string WhatsNew = "DevHome.ViewModels.WhatsNewViewModel"; public static readonly string Settings = "DevHome.Settings.ViewModels.SettingsViewModel"; public static readonly string Feedback = "DevHome.Settings.ViewModels.FeedbackViewModel"; + public static readonly string Environments = "DevHome.Environments.ViewModels.LandingPageViewModel"; + public static readonly string SetupFlow = "DevHome.SetupFlow.ViewModels.SetupFlowViewModel"; } diff --git a/common/Services/NotificationService.cs b/common/Services/NotificationService.cs index 2da857e01..72ff9e577 100644 --- a/common/Services/NotificationService.cs +++ b/common/Services/NotificationService.cs @@ -25,8 +25,6 @@ public class NotificationService private readonly IWindowsIdentityService _windowsIdentityService; - private readonly string _componentName = "NotificationService"; - private readonly string _hyperVText = "Hyper-V"; private readonly string _microsoftText = "Microsoft"; @@ -81,7 +79,7 @@ public void HandlerNotificationActions(AppActivationArguments args) } catch (Exception ex) { - _log.Error(_componentName, $"Unable to launch computer management due to exception", ex); + _log.Error(ex, $"Unable to launch computer management due to exception"); } } } @@ -124,7 +122,7 @@ public void ShowRestartNotification() } else { - _log.Error(_componentName, "Notification queue is not initialized"); + _log.Error("Notification queue is not initialized"); } } @@ -149,7 +147,7 @@ public void CheckIfUserIsAHyperVAdminAndShowNotification() var user = _windowsIdentityService.GetCurrentUserName(); if (user == null) { - _log.Error(_componentName, "Unable to get the current user name"); + _log.Error("Unable to get the current user name"); return; } @@ -185,7 +183,7 @@ public void CheckIfUserIsAHyperVAdminAndShowNotification() } catch (Exception ex) { - _log.Error(_componentName, "Unable to add the user to the Hyper-V Administrators group", ex); + _log.Error(ex, "Unable to add the user to the Hyper-V Administrators group"); ShowUnableToAddToHyperVAdminGroupNotification(); } }); @@ -209,7 +207,7 @@ public void CheckIfUserIsAHyperVAdminAndShowNotification() } else { - _log.Error(_componentName, "Notification queue is not initialized"); + _log.Error("Notification queue is not initialized"); } } } diff --git a/common/Strings/en-us/Resources.resw b/common/Strings/en-us/Resources.resw index 3cf7a21ab..93edddf01 100644 --- a/common/Strings/en-us/Resources.resw +++ b/common/Strings/en-us/Resources.resw @@ -233,7 +233,7 @@ <value>The current user is not a {0} administrators. {0} virtual machines will not load. Please add the user to the {0} administrators group and reboot.</value> <comment>Locked="{0}" Text explaining that the user is not in the {0} administrators group and that we need to add them.</comment> </data> - <data name="DevHomeActionDefaultText" xml:space="preserve"> + <data name="DevHomeActionDefaultText" xml:space="preserve"> <value>Open</value> <comment>Default text for a button that launches a content dialog in the UI</comment> </data> @@ -261,7 +261,7 @@ <value>Provider failed to provide a Header title for this item</value> <comment>Error text to be displayed when an provider does not provide header text for an item in a list of UI cards</comment> </data> - <data name="AdaptiveCardDialogPrimaryButtonText" xml:space="preserve"> + <data name="AdaptiveCardDialogPrimaryButtonText" xml:space="preserve"> <value>Ok</value> <comment>Primary button text that will be displayed to the user as the main action when they are presented with a content dialog with two buttons</comment> </data> @@ -277,4 +277,12 @@ <value>An item must be selected</value> <comment>Error text advising the user that they must select an item in the list before proceeding</comment> </data> + <data name="CreationOperationStoppedUnexpectedly" xml:space="preserve"> + <value>An unexpected error occured while attempting to create your new environment</value> + <comment>Error text for when there was an unexpected error that we could not handle within Dev Home.</comment> + </data> + <data name="GenericEnvironmentName" xml:space="preserve"> + <value>New Environment</value> + <comment>Generic name text for the card header for an environment that is being created in Dev Homes environment page</comment> + </data> </root> \ No newline at end of file diff --git a/common/Views/CloseButton.xaml b/common/Views/CloseButton.xaml index ffb3a11ed..41b0718d1 100644 --- a/common/Views/CloseButton.xaml +++ b/common/Views/CloseButton.xaml @@ -12,5 +12,6 @@ <TextBlock FontFamily="{ThemeResource SymbolThemeFontFamily}" FontSize="{ThemeResource BodyTextBlockFontSize}" + AutomationProperties.AccessibilityView="Raw" Text="" /> </Button> diff --git a/common/Windows/SecondaryWindow.cs b/common/Windows/SecondaryWindow.cs index 32794dc0d..845dbddfe 100644 --- a/common/Windows/SecondaryWindow.cs +++ b/common/Windows/SecondaryWindow.cs @@ -184,7 +184,7 @@ public SecondaryWindow() // Initialize window content template _windowTemplate = new(this); - WindowContent = _windowTemplate; + Content = _windowTemplate; // Register secondary window events handlers Activated += OnSecondaryWindowActivated; diff --git a/docs/ExperimentalFeatures.md b/docs/ExperimentalFeatures.md index 3bf567276..3b42f557f 100644 --- a/docs/ExperimentalFeatures.md +++ b/docs/ExperimentalFeatures.md @@ -24,7 +24,7 @@ This is useful for features that are not ready for general use, but can be teste "visible": true }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } diff --git a/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj b/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj index c207253f2..e6e3c5963 100644 --- a/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj +++ b/extensions/CoreWidgetProvider/CoreWidgetProvider.csproj @@ -12,8 +12,8 @@ <Nullable>enable</Nullable> <CsWinRTEnabled>false</CsWinRTEnabled> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> - <PublishProfileFullPath Condition="'$(BuildingInsideVisualStudio)' != 'True'">$(SolutionDir)\src\Properties\PublishProfiles\win10-$(Platform).pubxml</PublishProfileFullPath> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> + <PublishProfileFullPath Condition="'$(BuildingInsideVisualStudio)' != 'True'">$(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfileFullPath> </PropertyGroup> <ItemGroup> diff --git a/extensions/CoreWidgetProvider/Helpers/GPUStats.cs b/extensions/CoreWidgetProvider/Helpers/GPUStats.cs index 972c7320a..6cfaf019e 100644 --- a/extensions/CoreWidgetProvider/Helpers/GPUStats.cs +++ b/extensions/CoreWidgetProvider/Helpers/GPUStats.cs @@ -113,7 +113,7 @@ public void GetData() } catch (InvalidOperationException ex) { - Log.Warning("GPUStats", "Failed to get next value", ex); + Log.Warning(ex, "GPUStats", "Failed to get next value"); Log.Information("GPUStats", "Calling GetGPUPerfCounters again"); GetGPUPerfCounters(); } diff --git a/extensions/CoreWidgetProvider/Helpers/NetworkStats.cs b/extensions/CoreWidgetProvider/Helpers/NetworkStats.cs index 2b0f5b279..1b56389fa 100644 --- a/extensions/CoreWidgetProvider/Helpers/NetworkStats.cs +++ b/extensions/CoreWidgetProvider/Helpers/NetworkStats.cs @@ -87,7 +87,7 @@ public void GetData() } catch (Exception ex) { - Log.Error("Error getting network data.", ex); + Log.Error(ex, "Error getting network data."); } } } diff --git a/extensions/CoreWidgetProvider/Helpers/Resources.cs b/extensions/CoreWidgetProvider/Helpers/Resources.cs index 90e2144d1..85e5659de 100644 --- a/extensions/CoreWidgetProvider/Helpers/Resources.cs +++ b/extensions/CoreWidgetProvider/Helpers/Resources.cs @@ -23,7 +23,7 @@ public static string GetResource(string identifier, ILogger? log = null) } catch (Exception ex) { - log?.Error($"Failed loading resource: {identifier}", ex); + log?.Error(ex, $"Failed loading resource: {identifier}"); // If we fail, load the original identifier so it is obvious which resource is missing. return identifier; diff --git a/extensions/CoreWidgetProvider/Widgets/CoreWidget.cs b/extensions/CoreWidgetProvider/Widgets/CoreWidget.cs index c22304036..970080055 100644 --- a/extensions/CoreWidgetProvider/Widgets/CoreWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/CoreWidget.cs @@ -140,7 +140,7 @@ protected string GetTemplateForPage(WidgetPageState page) } catch (Exception e) { - Log.Error("Error getting template.", e); + Log.Error(e, "Error getting template."); return string.Empty; } } diff --git a/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs b/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs index f8623fd42..09818b1e0 100644 --- a/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SSHWalletWidget.cs @@ -83,7 +83,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving data.", e); + Log.Error(e, "Error retrieving data."); var content = new JsonObject { { "errorMessage", e.Message }, @@ -309,7 +309,7 @@ public override string GetConfiguration(string data) } catch (Exception ex) { - Log.Error($"Failed getting configuration information for input config file path: {data}", ex); + Log.Error(ex, $"Failed getting configuration information for input config file path: {data}"); configurationData = FillConfigurationData(false, data, 0, Resources.GetResource(@"SSH_Widget_Template/ErrorProcessingConfigFile", Log)); diff --git a/extensions/CoreWidgetProvider/Widgets/SystemCPUUsageWidget.cs b/extensions/CoreWidgetProvider/Widgets/SystemCPUUsageWidget.cs index bc6f137a2..6f5052fea 100644 --- a/extensions/CoreWidgetProvider/Widgets/SystemCPUUsageWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SystemCPUUsageWidget.cs @@ -56,7 +56,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving stats.", e); + Log.Error(e, "Error retrieving stats."); var content = new JsonObject { { "errorMessage", e.Message }, diff --git a/extensions/CoreWidgetProvider/Widgets/SystemGPUUsageWidget.cs b/extensions/CoreWidgetProvider/Widgets/SystemGPUUsageWidget.cs index c3f24fddf..2bd44d80b 100644 --- a/extensions/CoreWidgetProvider/Widgets/SystemGPUUsageWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SystemGPUUsageWidget.cs @@ -58,7 +58,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving data.", e); + Log.Error(e, "Error retrieving data."); var content = new JsonObject { { "errorMessage", e.Message }, diff --git a/extensions/CoreWidgetProvider/Widgets/SystemMemoryWidget.cs b/extensions/CoreWidgetProvider/Widgets/SystemMemoryWidget.cs index a24210b9e..88112f6ce 100644 --- a/extensions/CoreWidgetProvider/Widgets/SystemMemoryWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SystemMemoryWidget.cs @@ -75,7 +75,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving data.", e); + Log.Error(e, "Error retrieving data."); var content = new JsonObject { { "errorMessage", e.Message }, diff --git a/extensions/CoreWidgetProvider/Widgets/SystemNetworkUsageWidget.cs b/extensions/CoreWidgetProvider/Widgets/SystemNetworkUsageWidget.cs index 2e56c4744..1161487f4 100644 --- a/extensions/CoreWidgetProvider/Widgets/SystemNetworkUsageWidget.cs +++ b/extensions/CoreWidgetProvider/Widgets/SystemNetworkUsageWidget.cs @@ -76,7 +76,7 @@ public override void LoadContentData() } catch (Exception e) { - Log.Error("Error retrieving data.", e); + Log.Error(e, "Error retrieving data."); var content = new JsonObject { { "errorMessage", e.Message }, diff --git a/extensions/CoreWidgetProvider/Widgets/WidgetProvider.cs b/extensions/CoreWidgetProvider/Widgets/WidgetProvider.cs index 623bbdd3f..7f55948f3 100644 --- a/extensions/CoreWidgetProvider/Widgets/WidgetProvider.cs +++ b/extensions/CoreWidgetProvider/Widgets/WidgetProvider.cs @@ -64,7 +64,7 @@ private void RecoverRunningWidgets() } catch (Exception e) { - Log.Error("Failed retrieving list of running widgets.", e); + Log.Error(e, "Failed retrieving list of running widgets."); return; } diff --git a/extensions/CoreWidgetProvider/corewidgets_appsettings.json b/extensions/CoreWidgetProvider/corewidgets_appsettings.json index d598474f0..349755f1f 100644 --- a/extensions/CoreWidgetProvider/corewidgets_appsettings.json +++ b/extensions/CoreWidgetProvider/corewidgets_appsettings.json @@ -6,7 +6,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -14,7 +14,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\corewidgets.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/extensions/SampleExtension/SampleExtension.csproj b/extensions/SampleExtension/SampleExtension.csproj index 9c965fb4e..a67969dde 100644 --- a/extensions/SampleExtension/SampleExtension.csproj +++ b/extensions/SampleExtension/SampleExtension.csproj @@ -5,7 +5,7 @@ <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion> <RootNamespace>SampleExtension</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <UseWinUI>false</UseWinUI> <EnableMsixTooling>true</EnableMsixTooling> <CsWinRTEnabled>false</CsWinRTEnabled> diff --git a/settings/DevHome.Settings.UITest/DevHome.Dashboard.UITest.csproj b/settings/DevHome.Settings.UITest/DevHome.Dashboard.UITest.csproj index bfc44bfbe..6b676e84d 100644 --- a/settings/DevHome.Settings.UITest/DevHome.Dashboard.UITest.csproj +++ b/settings/DevHome.Settings.UITest/DevHome.Dashboard.UITest.csproj @@ -3,7 +3,7 @@ <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework> <RootNamespace>Dashboard.UITest</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <IsPackable>false</IsPackable> <ImplicitUsings>enable</ImplicitUsings> <UseWinUI>true</UseWinUI> diff --git a/settings/DevHome.Settings.UnitTest/DevHome.SetupFlow.UnitTest.csproj b/settings/DevHome.Settings.UnitTest/DevHome.SetupFlow.UnitTest.csproj index a71518263..e6f6a4f67 100644 --- a/settings/DevHome.Settings.UnitTest/DevHome.SetupFlow.UnitTest.csproj +++ b/settings/DevHome.Settings.UnitTest/DevHome.SetupFlow.UnitTest.csproj @@ -3,7 +3,7 @@ <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework> <RootNamespace>Dashboard.Test</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <IsPackable>false</IsPackable> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> diff --git a/settings/DevHome.Settings/DevHome.Settings.csproj b/settings/DevHome.Settings/DevHome.Settings.csproj index de2199882..dcb8cad91 100644 --- a/settings/DevHome.Settings/DevHome.Settings.csproj +++ b/settings/DevHome.Settings/DevHome.Settings.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.Settings</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Nullable>enable</Nullable> <UseWinUI>true</UseWinUI> </PropertyGroup> diff --git a/settings/DevHome.Settings/Strings/en-us/Resources.resw b/settings/DevHome.Settings/Strings/en-us/Resources.resw index c1dcbd8a1..d73581bd8 100644 --- a/settings/DevHome.Settings/Strings/en-us/Resources.resw +++ b/settings/DevHome.Settings/Strings/en-us/Resources.resw @@ -565,5 +565,13 @@ <data name="QuietBackgroundProcessesExperiment_Description" xml:space="preserve"> <value>Silence and track background processes that may hinder device performance</value> <comment>Inline description of the Quiet Background Processes experimental feature on the 'Settings -> Experiments' page where you enable it.</comment> + </data> + <data name="EnvironmentsCreationFlow_Description" xml:space="preserve"> + <value>Create a local or cloud machine from Dev Home</value> + <comment>Text within a display card that explains what users can do with the Environments creation feature. Users can choose to toggle this on/off to add or remove the feature.</comment> + </data> + <data name="EnvironmentsCreationFlow_Name" xml:space="preserve"> + <value>Environments Creation</value> + <comment>Title text for the Environments creation feature.</comment> </data> </root> \ No newline at end of file diff --git a/settings/DevHome.Settings/Views/AboutPage.xaml.cs b/settings/DevHome.Settings/Views/AboutPage.xaml.cs index ec2046d7a..743dc23bb 100644 --- a/settings/DevHome.Settings/Views/AboutPage.xaml.cs +++ b/settings/DevHome.Settings/Views/AboutPage.xaml.cs @@ -44,7 +44,7 @@ private void OpenLogsLocation() catch (Exception e) { var log = Log.ForContext("SourceContext", "AboutPage"); - log.Error($"Error opening log location", e); + log.Error(e, $"Error opening log location"); } } #endif diff --git a/settings/DevHome.Settings/Views/AccountsPage.xaml.cs b/settings/DevHome.Settings/Views/AccountsPage.xaml.cs index f37a358c6..427aa761b 100644 --- a/settings/DevHome.Settings/Views/AccountsPage.xaml.cs +++ b/settings/DevHome.Settings/Views/AccountsPage.xaml.cs @@ -114,7 +114,7 @@ public async Task ShowLoginUIAsync(string loginEntryPoint, Page parentPage, Acco } catch (Exception ex) { - _log.Error($"ShowLoginUIAsync(): loginUIContentDialog failed.", ex); + _log.Error(ex, $"ShowLoginUIAsync(): loginUIContentDialog failed."); } accountProvider.RefreshLoggedInAccounts(); @@ -186,7 +186,7 @@ private async Task InitiateAddAccountUserExperienceAsync(Page parentPage, Accoun } catch (Exception ex) { - _log.Error($"Exception thrown while calling {nameof(accountProvider.DeveloperIdProvider)}.{nameof(accountProvider.DeveloperIdProvider.ShowLogonSession)}: ", ex); + _log.Error(ex, $"Exception thrown while calling {nameof(accountProvider.DeveloperIdProvider)}.{nameof(accountProvider.DeveloperIdProvider.ShowLogonSession)}: "); } accountProvider.RefreshLoggedInAccounts(); diff --git a/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs b/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs index 5cb33dbf3..90f6f3607 100644 --- a/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs +++ b/settings/DevHome.Settings/Views/FeedbackPage.xaml.cs @@ -297,7 +297,7 @@ private string GetExtensions() var extensionsStr = stringResource.GetLocalized("Settings_Feedback_Extensions") + ": \n"; foreach (var extension in extensions) { - extensionsStr += extension.PackageFullName + "\n"; + extensionsStr += extension.PackageFullName + " (" + extension.ExtensionDisplayName + ")\n"; } return extensionsStr; diff --git a/src/App.xaml b/src/App.xaml index 8398c3552..d60987bf6 100644 --- a/src/App.xaml +++ b/src/App.xaml @@ -19,6 +19,7 @@ <ResourceDictionary Source="ms-appx:///DevHome.Dashboard/Styles/Dashboard_ThemeResources.xaml" /> <ResourceDictionary Source="ms-appx:///DevHome.SetupFlow/Styles/SetupFlow_ThemeResources.xaml" /> <ResourceDictionary Source="ms-appx:///DevHome.Common/Environments/Styles/HorizontalCardStyles.xaml" /> + <ResourceDictionary Source="ms-appx:///DevHome.Common/Views/AdaptiveCardViews/AdaptiveCardResourceTemplates.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> diff --git a/src/DevHome.csproj b/src/DevHome.csproj index 40ff0e7d1..d74929275 100644 --- a/src/DevHome.csproj +++ b/src/DevHome.csproj @@ -14,8 +14,8 @@ <ApplicationIcon Condition="'$(BuildRing)'=='Stable'">Assets\Preview\DevHome_Preview.ico</ApplicationIcon> <ApplicationManifest>app.manifest</ApplicationManifest> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> - <PublishProfile Condition="'$(BuildingInsideVisualStudio)' != 'True'">Properties\PublishProfiles\win10-$(Platform).pubxml</PublishProfile> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> + <PublishProfile Condition="'$(BuildingInsideVisualStudio)' != 'True'">Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <UseWinUI>true</UseWinUI> @@ -60,6 +60,9 @@ </ItemGroup> <ItemGroup> + <!-- Temporarily duplicate the Adaptive Card from DevHome.Common --> + <PackageReference Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" GeneratePathProperty="true" /> + <PackageReference Include="AdaptiveCards.Rendering.WinUI3" Version="2.0.0-beta" GeneratePathProperty="true" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Internal.Windows.DevHome.Helpers" Version="1.0.20240311-x1907" /> <PackageReference Include="Microsoft.Management.Infrastructure" Version="3.0.0" /> @@ -142,4 +145,14 @@ <DefineConstants Condition="'$(BuildRing)'=='Stable'">$(DefineConstants);STABLE_BUILD</DefineConstants> </PropertyGroup> + <!-- Workaround for Adaptive Cards not supporting correct RIDs when using .NET 8 --> + <ItemGroup> + <Content Include="$(TargetDir)\AdaptiveCards.ObjectModel.WinUI3.dll" Link="AdaptiveCards.ObjectModel.WinUI3.dll" CopyToOutputDirectory="PreserveNewest" /> + <Content Include="$(TargetDir)\AdaptiveCards.Rendering.WinUI3.dll" Link="AdaptiveCards.Rendering.WinUI3.dll" CopyToOutputDirectory="PreserveNewest" /> + </ItemGroup> + + <Target Name="CopyAdaptiveCardsToTargetDir" BeforeTargets="BeforeBuild"> + <Copy SourceFiles="$(PkgAdaptiveCards_ObjectModel_WinUI3)\runtimes\win10-$(Platform)\native\AdaptiveCards.ObjectModel.WinUI3.dll" DestinationFolder="$(TargetDir)" /> + <Copy SourceFiles="$(PkgAdaptiveCards_Rendering_WinUI3)\runtimes\win10-$(Platform)\native\AdaptiveCards.Rendering.WinUI3.dll" DestinationFolder="$(TargetDir)" /> + </Target> </Project> diff --git a/src/Models/ExtensionWrapper.cs b/src/Models/ExtensionWrapper.cs index 2b6552c63..7f5743aff 100644 --- a/src/Models/ExtensionWrapper.cs +++ b/src/Models/ExtensionWrapper.cs @@ -32,7 +32,8 @@ public class ExtensionWrapper : IExtensionWrapper public ExtensionWrapper(AppExtension appExtension, string classId) { - Name = appExtension.DisplayName; + PackageDisplayName = appExtension.Package.DisplayName; + ExtensionDisplayName = appExtension.DisplayName; PackageFullName = appExtension.Package.Id.FullName; PackageFamilyName = appExtension.Package.Id.FamilyName; ExtensionClassId = classId ?? throw new ArgumentNullException(nameof(classId)); @@ -42,40 +43,21 @@ public ExtensionWrapper(AppExtension appExtension, string classId) ExtensionUniqueId = appExtension.AppInfo.AppUserModelId + "!" + appExtension.Id; } - public string Name - { - get; - } + public string PackageDisplayName { get; } - public string PackageFullName - { - get; - } + public string ExtensionDisplayName { get; } - public string PackageFamilyName - { - get; - } + public string PackageFullName { get; } - public string ExtensionClassId - { - get; - } + public string PackageFamilyName { get; } - public string Publisher - { - get; - } + public string ExtensionClassId { get; } - public DateTimeOffset InstalledDate - { - get; - } + public string Publisher { get; } - public PackageVersion Version - { - get; - } + public DateTimeOffset InstalledDate { get; } + + public PackageVersion Version { get; } /// <summary> /// Gets the unique id for this Dev Home extension. The unique id is a concatenation of: @@ -86,10 +68,7 @@ public PackageVersion Version /// <item>The Extension Id. This is the unique identifier of the extension within the application.</item> /// </list> /// </summary> - public string ExtensionUniqueId - { - get; - } + public string ExtensionUniqueId { get; } public bool IsRunning() { diff --git a/src/NavConfig.jsonc b/src/NavConfig.jsonc index d134ce80b..c65878b80 100644 --- a/src/NavConfig.jsonc +++ b/src/NavConfig.jsonc @@ -45,7 +45,7 @@ { "buildType": "dev", "enabledByDefault": true, - "visible": true + "visible": false }, { "buildType": "canary", @@ -53,7 +53,7 @@ "visible": true }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } @@ -74,7 +74,7 @@ "visible": false }, { - "buildType": "release", + "buildType": "stable", "enabledByDefault": false, "visible": false } @@ -95,7 +95,28 @@ "visible": false }, { - "buildType": "release", + "buildType": "stable", + "enabledByDefault": false, + "visible": false + } + ] + }, + { + "identity": "EnvironmentsCreationFlow", + "enabledByDefault": false, + "buildTypeOverrides": [ + { + "buildType": "dev", + "enabledByDefault": true, + "visible": true + }, + { + "buildType": "canary", + "enabledByDefault": true, + "visible": true + }, + { + "buildType": "stable", "enabledByDefault": false, "visible": false } diff --git a/src/Package.appxmanifest b/src/Package.appxmanifest index 27b4f1e11..3e44bb0bc 100644 --- a/src/Package.appxmanifest +++ b/src/Package.appxmanifest @@ -84,8 +84,10 @@ <uap3:Properties> <DevHomeProvider> <Activation> - <CreateInstance ClassId="F8B26528-976A-488C-9B40-7198FB425C9E" /> + <CreateInstance ClassId="426A52D6-8007-4894-A946-CF80F39507F1" /> </Activation> + <!-- Best practice is to define SupportedInterfaces, even if it is empty --> + <SupportedInterfaces /> </DevHomeProvider> </uap3:Properties> </uap3:AppExtension> @@ -95,7 +97,7 @@ <uap3:Properties> <DevHomeProvider> <Activation> - <CreateInstance ClassId="426A52D6-8007-4894-A946-CF80F39507F1" /> + <CreateInstance ClassId="F8B26528-976A-488C-9B40-7198FB425C9E" /> </Activation> <SupportedInterfaces> <ComputeSystem /> @@ -296,4 +298,4 @@ <genTemplate:Item Name="platform" Value="WinUI" /> <genTemplate:Item Name="appmodel" Value="Desktop" /> </genTemplate:Metadata> -</Package> \ No newline at end of file +</Package> diff --git a/src/Properties/PublishProfiles/win10-arm64.pubxml b/src/Properties/PublishProfiles/win-arm64.pubxml similarity index 92% rename from src/Properties/PublishProfiles/win10-arm64.pubxml rename to src/Properties/PublishProfiles/win-arm64.pubxml index 87058c3fd..227cf8773 100644 --- a/src/Properties/PublishProfiles/win10-arm64.pubxml +++ b/src/Properties/PublishProfiles/win-arm64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>arm64</Platform> - <RuntimeIdentifier>win10-arm64</RuntimeIdentifier> + <RuntimeIdentifier>win-arm64</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/src/Properties/PublishProfiles/win10-x64.pubxml b/src/Properties/PublishProfiles/win-x64.pubxml similarity index 92% rename from src/Properties/PublishProfiles/win10-x64.pubxml rename to src/Properties/PublishProfiles/win-x64.pubxml index ab80eaaf1..19ae2a6b9 100644 --- a/src/Properties/PublishProfiles/win10-x64.pubxml +++ b/src/Properties/PublishProfiles/win-x64.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>x64</Platform> - <RuntimeIdentifier>win10-x64</RuntimeIdentifier> + <RuntimeIdentifier>win-x64</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/src/Properties/PublishProfiles/win10-x86.pubxml b/src/Properties/PublishProfiles/win-x86.pubxml similarity index 92% rename from src/Properties/PublishProfiles/win10-x86.pubxml rename to src/Properties/PublishProfiles/win-x86.pubxml index 5b0b0359a..dace1fa91 100644 --- a/src/Properties/PublishProfiles/win10-x86.pubxml +++ b/src/Properties/PublishProfiles/win-x86.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. <PropertyGroup> <PublishProtocol>FileSystem</PublishProtocol> <Platform>x86</Platform> - <RuntimeIdentifier>win10-x86</RuntimeIdentifier> + <RuntimeIdentifier>win-x86</RuntimeIdentifier> <SelfContained>true</SelfContained> <PublishSingleFile>False</PublishSingleFile> <PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> diff --git a/src/Services/AccountsService.cs b/src/Services/AccountsService.cs index 7127352bf..9574232fb 100644 --- a/src/Services/AccountsService.cs +++ b/src/Services/AccountsService.cs @@ -59,7 +59,7 @@ public async Task<IReadOnlyList<IDeveloperIdProvider>> GetDevIdProviders() } catch (Exception ex) { - _log.Error($"Failed to get {nameof(IDeveloperIdProvider)} provider from '{extension.Name}'", ex); + _log.Error(ex, $"Failed to get {nameof(IDeveloperIdProvider)} provider from '{extension.PackageFamilyName}/{extension.ExtensionDisplayName}'"); } } diff --git a/src/Services/DSCFileActivationHandler.cs b/src/Services/DSCFileActivationHandler.cs index 2c366c7ce..173168432 100644 --- a/src/Services/DSCFileActivationHandler.cs +++ b/src/Services/DSCFileActivationHandler.cs @@ -98,7 +98,7 @@ await _mainWindow.ShowErrorMessageDialogAsync( } catch (Exception ex) { - _log.Error("Error executing the DSC activation flow", ex); + _log.Error(ex, "Error executing the DSC activation flow"); } } } diff --git a/src/Services/ExtensionService.cs b/src/Services/ExtensionService.cs index 0566a581b..032d45c2c 100644 --- a/src/Services/ExtensionService.cs +++ b/src/Services/ExtensionService.cs @@ -313,12 +313,12 @@ protected virtual void Dispose(bool disposing) private IPropertySet? GetSubPropertySet(IPropertySet propSet, string name) { - return propSet[name] as IPropertySet; + return propSet.TryGetValue(name, out var value) ? value as IPropertySet : null; } private object[]? GetSubPropertySetArray(IPropertySet propSet, string name) { - return propSet[name] as object[]; + return propSet.TryGetValue(name, out var value) ? value as object[] : null; } /// <summary> @@ -330,7 +330,6 @@ private List<string> GetCreateInstanceList(IPropertySet activationPropSet) { var propSetList = new List<string>(); var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty); - var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); if (singlePropertySet != null) { var classId = GetProperty(singlePropertySet, ClassIdProperty); @@ -341,19 +340,23 @@ private List<string> GetCreateInstanceList(IPropertySet activationPropSet) propSetList.Add(classId); } } - else if (propertySetArray != null) + else { - foreach (var prop in propertySetArray) + var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); + if (propertySetArray != null) { - if (prop is not IPropertySet propertySet) + foreach (var prop in propertySetArray) { - continue; - } + if (prop is not IPropertySet propertySet) + { + continue; + } - var classId = GetProperty(propertySet, ClassIdProperty); - if (classId != null) - { - propSetList.Add(classId); + var classId = GetProperty(propertySet, ClassIdProperty); + if (classId != null) + { + propSetList.Add(classId); + } } } } diff --git a/src/ViewModels/InitializationViewModel.cs b/src/ViewModels/InitializationViewModel.cs index 025c6c616..7c0b33a0c 100644 --- a/src/ViewModels/InitializationViewModel.cs +++ b/src/ViewModels/InitializationViewModel.cs @@ -65,7 +65,7 @@ public async void OnPageLoaded() } catch (Exception ex) { - _log.Information("Installing WidgetService failed: ", ex); + _log.Information(ex, "Installing WidgetService failed: "); } // Install the DevHomeGitHubExtension, unless it's already installed or a dev build is running. @@ -82,7 +82,7 @@ public async void OnPageLoaded() } catch (Exception ex) { - _log.Information("Installing DevHomeGitHubExtension failed: ", ex); + _log.Information(ex, "Installing DevHomeGitHubExtension failed: "); } } diff --git a/src/Views/ShellPage.xaml b/src/Views/ShellPage.xaml index 37b0b4086..2b58a8559 100644 --- a/src/Views/ShellPage.xaml +++ b/src/Views/ShellPage.xaml @@ -126,7 +126,7 @@ from reading the control value. --> <TextBlock Width="0" Height="0" IsTabStop="False" Text="{x:Bind ViewModel.AnnouncementText, Mode=OneWay}"> <i:Interaction.Behaviors> - <behaviors:TextBlockAutomationBehavior RaiseLiveRegionChangedEvent="True" LiveSetting="Polite" /> + <behaviors:TextBlockAutomationBehavior RaiseLiveRegionChangedEvent="True" LiveSetting="Assertive" /> </i:Interaction.Behaviors> </TextBlock> </Grid> diff --git a/src/appsettings.json b/src/appsettings.json index d118f3fcc..b16cd33ca 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -15,7 +15,7 @@ { "Name": "Console", "Args": { - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Debug" } }, @@ -23,7 +23,7 @@ "Name": "File", "Args": { "path": "%DEVHOME_LOGS_ROOT%\\devhome.dhlog", - "outputTemplate": "[{Timestamp:yyyy/mm/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", + "outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}", "restrictedToMinimumLevel": "Information", "rollingInterval": "Day" } diff --git a/telemetry/DevHome.Telemetry/DevHome.Telemetry.csproj b/telemetry/DevHome.Telemetry/DevHome.Telemetry.csproj index b56449c53..bd827b8dc 100644 --- a/telemetry/DevHome.Telemetry/DevHome.Telemetry.csproj +++ b/telemetry/DevHome.Telemetry/DevHome.Telemetry.csproj @@ -1,19 +1,19 @@ -<Project Sdk="Microsoft.NET.Sdk"> - <Import Project="$(SolutionDir)ToolingVersions.props" /> - <PropertyGroup> - <RootNamespace>DevHome.Telemetry</RootNamespace> - <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> - <UseWinUI>true</UseWinUI> - <!-- DefineConstants is removed in Unstub.ps1--> - <DefineConstants>TELEMETRYEVENTSOURCE_PUBLIC</DefineConstants> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Serilog" Version="3.1.1" /> - <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> - <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" /> - <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> - <PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" /> - <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> - </ItemGroup> +<Project Sdk="Microsoft.NET.Sdk"> + <Import Project="$(SolutionDir)ToolingVersions.props" /> + <PropertyGroup> + <RootNamespace>DevHome.Telemetry</RootNamespace> + <Platforms>x86;x64;arm64</Platforms> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> + <UseWinUI>true</UseWinUI> + <!-- DefineConstants is removed in Unstub.ps1--> + <DefineConstants>TELEMETRYEVENTSOURCE_PUBLIC</DefineConstants> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Serilog" Version="3.1.1" /> + <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> + <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" /> + <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> + <PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" /> + <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> + </ItemGroup> </Project> \ No newline at end of file diff --git a/test/DevHome.Test.csproj b/test/DevHome.Test.csproj index 05dc1101f..5027457d7 100644 --- a/test/DevHome.Test.csproj +++ b/test/DevHome.Test.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.Test</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <IsPackable>false</IsPackable> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> diff --git a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj index d0dd34f2d..e99af82cc 100644 --- a/tools/Customization/DevHome.Customization/DevHome.Customization.csproj +++ b/tools/Customization/DevHome.Customization/DevHome.Customization.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.Customization</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Nullable>enable</Nullable> <UseWinUI>true</UseWinUI> </PropertyGroup> diff --git a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs index a309f9182..b06769e22 100644 --- a/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs +++ b/tools/Customization/DevHome.Customization/Extensions/ServiceExtensions.cs @@ -19,7 +19,7 @@ public static IServiceCollection AddWindowsCustomization(this IServiceCollection services.AddSingleton<DeveloperFileExplorerViewModel>(); services.AddTransient<DeveloperFileExplorerPage>(); - services.AddSingleton<OptimizeDevDriveDialogViewModelFactory>(sp => (cacheLocation, environmentVariable) => ActivatorUtilities.CreateInstance<OptimizeDevDriveDialogViewModel>(sp, cacheLocation, environmentVariable)); + services.AddSingleton<OptimizeDevDriveDialogViewModelFactory>(sp => (cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters) => ActivatorUtilities.CreateInstance<OptimizeDevDriveDialogViewModel>(sp, cacheLocation, environmentVariable, exampleDevDriveLocation, existingDevDriveLetters)); services.AddSingleton<DevDriveInsightsViewModel>(); services.AddTransient<DevDriveInsightsPage>(); diff --git a/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs b/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs index 8fb1762a1..8b9223c3c 100644 --- a/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs +++ b/tools/Customization/DevHome.Customization/Helpers/DevDriveCacheData.cs @@ -17,5 +17,5 @@ public partial class DevDriveCacheData public List<string>? CacheDirectory { get; set; } - public string? ExampleDirectory { get; set; } + public string? ExampleSubDirectory { get; set; } } diff --git a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw index d6a6c7a6f..3914c8eb0 100644 --- a/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw +++ b/tools/Customization/DevHome.Customization/Strings/en-us/Resources.resw @@ -126,7 +126,7 @@ <comment>Dev drive size free</comment> </data> <data name="DevDriveInsightsCard.Description" xml:space="preserve"> - <value>All things, dev drives, optimizations, etc.</value> + <value>All things, Dev Drives, optimizations, etc.</value> <comment>The description for the Dev Drive Insights settings card</comment> </data> <data name="DevDriveInsightsCard.Header" xml:space="preserve"> @@ -161,9 +161,9 @@ <value>Enable end task in taskbar by right click</value> <comment>The description for the end task on task bar settings card</comment> </data> - <data name="ExampleDevDriveLocation" xml:space="preserve"> - <value>Example: E:\packages\pip</value> - <comment>Example dev drive location</comment> + <data name="ExampleText" xml:space="preserve"> + <value>Example: </value> + <comment>Example string, will be followed by a sample location to move the cache to a dev drive location</comment> </data> <data name="EndTaskOnTaskBar.Header" xml:space="preserve"> <value>End Task</value> diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs index aa64ba3f0..e0175b81d 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/DevDriveOptimizerCardViewModel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -18,6 +19,8 @@ public partial class DevDriveOptimizerCardViewModel : ObservableObject { public OptimizeDevDriveDialogViewModelFactory OptimizeDevDriveDialogViewModelFactory { get; set; } + public List<string> ExistingDevDriveLetters { get; set; } + public string CacheToBeMoved { get; set; } public string DevDriveOptimizationSuggestion { get; set; } @@ -41,7 +44,11 @@ private async Task OptimizeDevDriveAsync(object sender) var settingsCard = sender as Button; if (settingsCard != null) { - var optimizeDevDriveViewModel = OptimizeDevDriveDialogViewModelFactory(ExistingCacheLocation, EnvironmentVariableToBeSet); + var optimizeDevDriveViewModel = OptimizeDevDriveDialogViewModelFactory( + ExistingCacheLocation, + EnvironmentVariableToBeSet, + ExampleLocationOnDevDrive, + ExistingDevDriveLetters); var optimizeDevDriveDialog = new OptimizeDevDriveDialog(optimizeDevDriveViewModel); optimizeDevDriveDialog.XamlRoot = settingsCard.XamlRoot; optimizeDevDriveDialog.RequestedTheme = settingsCard.ActualTheme; @@ -49,9 +56,16 @@ private async Task OptimizeDevDriveAsync(object sender) } } - public DevDriveOptimizerCardViewModel(OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory, string cacheToBeMoved, string existingCacheLocation, string exampleLocationOnDevDrive, string environmentVariableToBeSet) + public DevDriveOptimizerCardViewModel( + OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory, + string cacheToBeMoved, + string existingCacheLocation, + string exampleLocationOnDevDrive, + string environmentVariableToBeSet, + List<string> existingDevDriveLetters) { OptimizeDevDriveDialogViewModelFactory = optimizeDevDriveDialogViewModelFactory; + ExistingDevDriveLetters = existingDevDriveLetters; CacheToBeMoved = cacheToBeMoved; ExistingCacheLocation = existingCacheLocation; ExampleLocationOnDevDrive = exampleLocationOnDevDrive; diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs index 2d0cedc5c..340da507e 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsights/OptimizeDevDriveDialogViewModel.cs @@ -2,15 +2,22 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; +using System.Data.SqlTypes; using System.IO; 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.Telemetry; +using Microsoft.UI.Xaml.Controls; using Serilog; +using Windows.Media.Protection; using Windows.Storage.Pickers; using WinUIEx; +using static Microsoft.Extensions.Logging.EventSource.LoggingEventSource; namespace DevHome.Customization.ViewModels.DevDriveInsights; @@ -19,6 +26,9 @@ namespace DevHome.Customization.ViewModels.DevDriveInsights; /// </summary> public partial class OptimizeDevDriveDialogViewModel : ObservableObject { + [ObservableProperty] + private List<string> _existingDevDriveLetters; + [ObservableProperty] private string _exampleDevDriveLocation; @@ -40,11 +50,16 @@ public partial class OptimizeDevDriveDialogViewModel : ObservableObject [ObservableProperty] private string _directoryPathTextBox; - public OptimizeDevDriveDialogViewModel(string existingCacheLocation, string environmentVariableToBeSet) + public OptimizeDevDriveDialogViewModel( + string existingCacheLocation, + string environmentVariableToBeSet, + string exampleDevDriveLocation, + List<string> existingDevDriveLetters) { DirectoryPathTextBox = string.Empty; var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); - ExampleDevDriveLocation = stringResource.GetLocalized("ExampleDevDriveLocation"); + ExistingDevDriveLetters = existingDevDriveLetters; + ExampleDevDriveLocation = stringResource.GetLocalized("ExampleText") + exampleDevDriveLocation; ChooseDirectoryPromptText = stringResource.GetLocalized("ChooseDirectoryPromptText"); MakeChangesText = stringResource.GetLocalized("MakeChangesText"); ExistingCacheLocation = existingCacheLocation; @@ -78,7 +93,20 @@ private async Task BrowseButtonClick(object sender) } } - private void MoveDirectory(string sourceDirectory, string targetDirectory) + private string RemovePrivacyInfo(string input) + { + var output = input; + var userProfilePath = Environment.ExpandEnvironmentVariables("%userprofile%"); + if (input.StartsWith(userProfilePath, StringComparison.OrdinalIgnoreCase)) + { + var index = input.LastIndexOf(userProfilePath, StringComparison.OrdinalIgnoreCase) + userProfilePath.Length; + output = Path.Join("%userprofile%", input.Substring(index)); + } + + return output; + } + + private bool MoveDirectory(string sourceDirectory, string targetDirectory) { try { @@ -110,10 +138,13 @@ private void MoveDirectory(string sourceDirectory, string targetDirectory) // Delete the source directory Directory.Delete(sourceDirectory, true); + return true; } catch (Exception ex) { Log.Error($"Error in MoveDirectory. Error: {ex}"); + TelemetryFactory.Get<ITelemetry>().LogError("DevDriveInsights_PackageCacheMoveDirectory_Error", LogLevel.Critical, new ExceptionEvent(ex.HResult, RemovePrivacyInfo(sourceDirectory))); + return false; } } @@ -125,8 +156,21 @@ private void SetEnvironmentVariable(string variableName, string value) } catch (Exception ex) { - Log.Error($"Error in SetEnvironmentVariable. Error: {ex}"); + Log.Error(ex, $"Error in SetEnvironmentVariable. Error: {ex}"); + } + } + + private bool ChosenDirectoryInDevDrive(string directoryPath) + { + foreach (var devDriveLetter in ExistingDevDriveLetters) + { + if (directoryPath.StartsWith(devDriveLetter + ":", StringComparison.OrdinalIgnoreCase)) + { + return true; + } } + + return false; } [RelayCommand] @@ -137,9 +181,21 @@ private void DirectoryInputConfirmed() if (!string.IsNullOrEmpty(directoryPath)) { // Handle the selected folder - // TODO: If chosen folder not a dev drive location, currently we no-op. Instead we should display the error. - MoveDirectory(ExistingCacheLocation, directoryPath); - SetEnvironmentVariable(EnvironmentVariableToBeSet, directoryPath); + // TODO: If chosen folder not a dev drive location, currently we no-op and log the error. Instead we should display the error. + if (ChosenDirectoryInDevDrive(directoryPath)) + { + if (MoveDirectory(ExistingCacheLocation, directoryPath)) + { + SetEnvironmentVariable(EnvironmentVariableToBeSet, directoryPath); + var existingCacheLocationVetted = RemovePrivacyInfo(ExistingCacheLocation); + Log.Debug($"Moved cache from {existingCacheLocationVetted} to {directoryPath}"); + TelemetryFactory.Get<ITelemetry>().Log("DevDriveInsights_PackageCacheMovedSuccessfully_Event", LogLevel.Critical, new ExceptionEvent(0, existingCacheLocationVetted)); + } + } + else + { + Log.Error($"Chosen directory {directoryPath} not on a dev drive."); + } } } } diff --git a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs index 5e3fa54bd..15000d4f2 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/DevDriveInsightsViewModel.cs @@ -12,12 +12,17 @@ using DevHome.Customization.Helpers; using DevHome.Customization.ViewModels.DevDriveInsights; using DevHome.Customization.Views; +using Microsoft.Internal.Windows.DevHome.Helpers; using Serilog; namespace DevHome.Customization.ViewModels; public partial class DevDriveInsightsViewModel : ObservableObject { + private readonly ShellSettings _shellSettings; + + public ObservableCollection<Breadcrumb> Breadcrumbs { get; } + public ObservableCollection<DevDriveCardViewModel> DevDriveCardCollection { get; private set; } = new(); public ObservableCollection<DevDriveOptimizerCardViewModel> DevDriveOptimizerCardCollection { get; private set; } = new(); @@ -48,12 +53,29 @@ public partial class DevDriveInsightsViewModel : ObservableObject private IEnumerable<IDevDrive> ExistingDevDrives { get; set; } = Enumerable.Empty<IDevDrive>(); + private static readonly string _appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + private static readonly string _localAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); private static readonly string _userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + private const string PackagesStr = "packages"; + + private const string CacheStr = "cache"; + + private const string ArchivesStr = "archives"; + public DevDriveInsightsViewModel(IDevDriveManager devDriveManager, OptimizeDevDriveDialogViewModelFactory optimizeDevDriveDialogViewModelFactory) { + _shellSettings = new ShellSettings(); + + var stringResource = new StringResource("DevHome.Customization.pri", "DevHome.Customization/Resources"); + Breadcrumbs = + [ + new(stringResource.GetLocalized("MainPage_Header"), typeof(MainPageViewModel).FullName!), + new(stringResource.GetLocalized("DevDriveInsights_Header"), typeof(DevDriveInsightsViewModel).FullName!) + ]; + _optimizeDevDriveDialogViewModelFactory = optimizeDevDriveDialogViewModelFactory; DevDriveManagerObj = devDriveManager; } @@ -175,7 +197,7 @@ public void LoadAllDevDrivesInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drives data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drives data. Error: {ex}"); } } @@ -195,7 +217,7 @@ public void LoadAllDevDriveOptimizersInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drive Optimizers data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drive Optimizers data. Error: {ex}"); } } @@ -215,7 +237,7 @@ public void LoadAllDevDriveOptimizedsInTheUI() } catch (Exception ex) { - Log.Error($"Error loading Dev Drive Optimized data. Error: {ex}"); + Log.Error(ex, $"Error loading Dev Drive Optimized data. Error: {ex}"); } } @@ -237,17 +259,60 @@ public void UpdateListViewModelList() EnvironmentVariable = "PIP_CACHE_DIR", CacheDirectory = new List<string> { - Path.Join(_localAppDataPath, "pip", "cache"), - Path.Join(_localAppDataPath, "packages", "PythonSoftwareFoundation.Python"), + Path.Join(_localAppDataPath, "pip", CacheStr), + Path.Join(_localAppDataPath, PackagesStr, "PythonSoftwareFoundation.Python"), }, - ExampleDirectory = Path.Join("D:", "packages", "pip", "cache"), + ExampleSubDirectory = Path.Join(PackagesStr, "pip", CacheStr), }, new DevDriveCacheData { CacheName = "NuGet cache (dotnet)", EnvironmentVariable = "NUGET_PACKAGES", - CacheDirectory = new List<string> { Path.Join(_userProfilePath, ".nuget", "packages") }, - ExampleDirectory = Path.Join("D:", "packages", "NuGet", "Cache"), + CacheDirectory = new List<string> { Path.Join(_userProfilePath, ".nuget", PackagesStr) }, + ExampleSubDirectory = Path.Join(PackagesStr, "NuGet", CacheStr), + }, + new DevDriveCacheData + { + CacheName = "Npm cache (NodeJS)", + EnvironmentVariable = "NPM_CONFIG_CACHE", + CacheDirectory = new List<string> + { + Path.Join(_appDataPath, "npm-cache"), + Path.Join(_localAppDataPath, "npm-cache"), + }, + ExampleSubDirectory = Path.Join(PackagesStr, "npm"), + }, + new DevDriveCacheData + { + CacheName = "Vcpkg cache", + EnvironmentVariable = "VCPKG_DEFAULT_BINARY_CACHE", + CacheDirectory = new List<string> + { + Path.Join(_appDataPath, "vcpkg", ArchivesStr), + Path.Join(_localAppDataPath, "vcpkg", ArchivesStr), + }, + ExampleSubDirectory = Path.Join(PackagesStr, "vcpkg"), + }, + new DevDriveCacheData + { + CacheName = "Cargo cache (Rust)", + EnvironmentVariable = "CARGO_HOME", + CacheDirectory = new List<string> { Path.Join(_userProfilePath, ".cargo") }, + ExampleSubDirectory = Path.Join(PackagesStr, "cargo"), + }, + new DevDriveCacheData + { + CacheName = "Maven cache (Java)", + EnvironmentVariable = "MAVEN_OPTS", + CacheDirectory = new List<string> { Path.Join(_userProfilePath, ".m2") }, + ExampleSubDirectory = Path.Join(PackagesStr, "m2"), + }, + new DevDriveCacheData + { + CacheName = "Gradle cache (Java)", + EnvironmentVariable = "GRADLE_USER_HOME", + CacheDirectory = new List<string> { Path.Join(_userProfilePath, ".gradle") }, + ExampleSubDirectory = Path.Join(PackagesStr, "gradle"), } ]; @@ -261,13 +326,13 @@ public void UpdateListViewModelList() } else { - var subDirectories = Directory.GetDirectories(_localAppDataPath + "\\Packages", "*", SearchOption.TopDirectoryOnly); + var subDirectories = Directory.GetDirectories(Path.Join(_localAppDataPath, PackagesStr), "*", SearchOption.TopDirectoryOnly); var matchingSubdirectory = subDirectories.FirstOrDefault(subdir => subdir.StartsWith(cacheDirectory, StringComparison.OrdinalIgnoreCase)); if (Directory.Exists(matchingSubdirectory)) { if (matchingSubdirectory.Contains("PythonSoftwareFoundation")) { - return Path.Join(matchingSubdirectory, "LocalCache", "Local", "pip", "cache"); + return Path.Join(matchingSubdirectory, "LocalCache", "Local", "pip", CacheStr); } return matchingSubdirectory; @@ -307,12 +372,16 @@ public void UpdateOptimizerListViewModelList() continue; } + List<string> existingDevDriveLetters = ExistingDevDrives.Select(x => x.DriveLetter.ToString()).ToList(); + + var exampleDirectory = Path.Join(existingDevDriveLetters[0] + ":", cache.ExampleSubDirectory); var card = new DevDriveOptimizerCardViewModel( _optimizeDevDriveDialogViewModelFactory, cache.CacheName!, existingCacheLocation, - cache.ExampleDirectory!, // example location on dev drive to move cache to - cache.EnvironmentVariable!); // environmentVariableToBeSet + exampleDirectory!, // example location on dev drive to move cache to + cache.EnvironmentVariable!, // environmentVariableToBeSet + existingDevDriveLetters); DevDriveOptimizerCardCollection.Add(card); } diff --git a/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs index cff6dc052..b54ea197a 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/MainPageViewModel.cs @@ -3,12 +3,14 @@ using System; using System.Collections.ObjectModel; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DevHome.Common.Extensions; using DevHome.Common.Models; using DevHome.Common.Services; +using Microsoft.UI.Xaml; using Windows.System; namespace DevHome.Customization.ViewModels; @@ -45,4 +47,6 @@ private void NavigateToDevDriveInsightsPage() { NavigationService.NavigateTo(typeof(DevDriveInsightsViewModel).FullName!); } + + public bool AnyDevDrivesPresent => Application.Current.GetService<IDevDriveManager>().GetAllDevDrivesThatExistOnSystem().Any(); } diff --git a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs b/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs index 11a9ac4b8..c7d360562 100644 --- a/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs +++ b/tools/Customization/DevHome.Customization/ViewModels/QuietBackgroundProcessesViewModel.cs @@ -110,7 +110,7 @@ public void QuietButtonClicked() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::Start failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::Start failed"); } } else @@ -124,7 +124,7 @@ public void QuietButtonClicked() catch (Exception ex) { SessionStateText = GetStatusString("UnableToCancelSession"); - _log.Error("QuietBackgroundProcessesSession::Stop failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::Stop failed"); } } } @@ -142,7 +142,7 @@ private bool GetIsActive() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::IsActive failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::IsActive failed"); } return false; @@ -157,7 +157,7 @@ private int GetTimeRemaining() catch (Exception ex) { SessionStateText = GetStatusString("SessionError"); - _log.Error("QuietBackgroundProcessesSession::TimeLeftInSeconds failed", ex); + _log.Error(ex, "QuietBackgroundProcessesSession::TimeLeftInSeconds failed"); return 0; } } diff --git a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml index abce444f8..e9ad08513 100644 --- a/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml +++ b/tools/Customization/DevHome.Customization/Views/DevDriveInsightsPage.xaml @@ -1,22 +1,15 @@ -<Page - x:Class="DevHome.Customization.Views.DevDriveInsightsPage" - xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:behaviors="using:DevHome.Common.Behaviors" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:views="using:DevHome.Customization.Views" - behaviors:NavigationViewHeaderBehavior.HeaderMode="Always" - mc:Ignorable="d"> - - <Grid MaxWidth="{ThemeResource MaxPageContentWidth}" Margin="{ThemeResource ContentPageMargin}"> - <Grid.RowDefinitions> - <RowDefinition Height="Auto" /> - <RowDefinition Height="Auto" /> - </Grid.RowDefinitions> - - <ScrollView Grid.Row="1" VerticalAlignment="Top"> - <views:DevDriveInsightsView /> - </ScrollView> - </Grid> -</Page> +<Page + x:Class="DevHome.Customization.Views.DevDriveInsightsPage" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:behaviors="using:DevHome.Common.Behaviors" + xmlns:views="using:DevHome.Customization.Views" + behaviors:NavigationViewHeaderBehavior.HeaderTemplate="{StaticResource BreadcrumbBarDataTemplate}" + behaviors:NavigationViewHeaderBehavior.HeaderContext="{x:Bind ViewModel}"> + + <ScrollView VerticalAlignment="Top"> + <Grid MaxWidth="{ThemeResource MaxPageContentWidth}" Margin="{ThemeResource ContentPageMargin}"> + <views:DevDriveInsightsView /> + </Grid> + </ScrollView> +</Page> diff --git a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml index 64ad93ec4..c7b8080aa 100644 --- a/tools/Customization/DevHome.Customization/Views/MainPageView.xaml +++ b/tools/Customization/DevHome.Customization/Views/MainPageView.xaml @@ -22,6 +22,7 @@ AutomationProperties.AccessibilityView="Control" AutomationProperties.AutomationId="NavigateDevDriveInsightsCardButton" Command="{x:Bind ViewModel.NavigateToDevDriveInsightsPageCommand}" + Visibility="{x:Bind ViewModel.AnyDevDrivesPresent}" IsClickEnabled="True" > <controls:SettingsCard.HeaderIcon> <FontIcon Glyph="" FontFamily="{ThemeResource AmcFluentIcons}"/> diff --git a/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs b/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs index 8191dc757..b6cde57e4 100644 --- a/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs +++ b/tools/Customization/DevHome.Customization/Views/OptimizeDevDriveDialog.xaml.cs @@ -1,12 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using DevHome.Customization.ViewModels.DevDriveInsights; using Microsoft.UI.Xaml.Controls; namespace DevHome.Customization.Views; -public delegate OptimizeDevDriveDialogViewModel OptimizeDevDriveDialogViewModelFactory(string existingCacheLocation, string environmentVariableToBeSet); +public delegate OptimizeDevDriveDialogViewModel OptimizeDevDriveDialogViewModelFactory( + string existingCacheLocation, + string environmentVariableToBeSet, + string exampleDevDriveLocation, + List<string> existingDevDriveLetters); public sealed partial class OptimizeDevDriveDialog : ContentDialog { diff --git a/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj b/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj index fb618c002..32d898a42 100644 --- a/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj +++ b/tools/Dashboard/DevHome.Dashboard.UnitTest/DevHome.Dashboard.UnitTest.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>Dashboard.Test</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <IsPackable>false</IsPackable> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs b/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs index 10237c394..9af21e4ed 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/SelectableMenuFlyoutItem.cs @@ -47,7 +47,7 @@ public void RemoveFromSelection() public void Select() { - IsSelected = true; + Invoke(); } protected override string GetClassNameCore() diff --git a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs index d3b607bea..b4a2471d8 100644 --- a/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Controls/WidgetControl.xaml.cs @@ -105,6 +105,8 @@ private async void OnRemoveWidgetClick(object sender, RoutedEventArgs e) var widgetIdToDelete = widgetViewModel.Widget.Id; var widgetToDelete = widgetViewModel.Widget; _log.Debug($"User removed widget, delete widget {widgetIdToDelete}"); + var stringResource = new StringResource("DevHome.Dashboard.pri", "DevHome.Dashboard/Resources"); + Application.Current.GetService<IScreenReaderService>().Announce(stringResource.GetLocalized("WidgetRemoved")); DashboardView.PinnedWidgets.Remove(widgetViewModel); try { @@ -113,7 +115,7 @@ private async void OnRemoveWidgetClick(object sender, RoutedEventArgs e) } catch (Exception ex) { - _log.Error($"Didn't delete Widget {widgetIdToDelete}", ex); + _log.Error(ex, $"Didn't delete Widget {widgetIdToDelete}"); } } } @@ -202,7 +204,7 @@ private void MarkSize(SelectableMenuFlyoutItem menuSizeItem) }; menuSizeItem.Icon = fontIcon; var peer = FrameworkElementAutomationPeer.FromElement(menuSizeItem) as SelectableMenuFlyoutItemAutomationPeer; - peer.Select(); + peer.AddToSelection(); } private void AddCustomizeToWidgetMenu(MenuFlyout widgetMenuFlyout, WidgetViewModel widgetViewModel) diff --git a/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj b/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj index 4c193d6e6..c3c1abe0b 100644 --- a/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj +++ b/tools/Dashboard/DevHome.Dashboard/DevHome.Dashboard.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.Dashboard</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <UseWinUI>true</UseWinUI> <CsWinRTIncludes>Microsoft.Windows.Widgets.Hosts</CsWinRTIncludes> </PropertyGroup> diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs index 9295b6584..30d5e7741 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetAdaptiveCardRenderingService.cs @@ -115,7 +115,7 @@ private async Task UpdateHostConfig() } catch (Exception ex) { - _log.Error("Error retrieving HostConfig", ex); + _log.Error(ex, "Error retrieving HostConfig"); } _windowEx.DispatcherQueue.TryEnqueue(() => diff --git a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs index 175e47f5a..2099ed7be 100644 --- a/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs +++ b/tools/Dashboard/DevHome.Dashboard/Services/WidgetHostingService.cs @@ -119,7 +119,7 @@ public async Task<WidgetHost> GetWidgetHostAsync() } catch (Exception ex) { - _log.Error("Exception in WidgetHost.Register:", ex); + _log.Error(ex, "Exception in WidgetHost.Register:"); } } @@ -136,7 +136,7 @@ public async Task<WidgetCatalog> GetWidgetCatalogAsync() } catch (Exception ex) { - _log.Error("Exception in WidgetCatalog.GetDefault:", ex); + _log.Error(ex, "Exception in WidgetCatalog.GetDefault:"); } } diff --git a/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw b/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw index b1231169a..5ca669270 100644 --- a/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw +++ b/tools/Dashboard/DevHome.Dashboard/Strings/en-us/Resources.resw @@ -1,5 +1,64 @@ <?xml version="1.0" encoding="utf-8"?> <root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> <xsd:element name="root" msdata:IsDataSet="true"> @@ -190,4 +249,12 @@ <value>Close</value> <comment>Text for a button to close a content dialog.</comment> </data> + <data name="DashboardPage.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> + <value>Dashboard</value> + <comment>Dashboard accessible name to be narrated</comment> + </data> + <data name="WidgetRemoved" xml:space="preserve"> + <value>Widget removed</value> + <comment>This is said by narrator whenever a widget is removed</comment> + </data> </root> diff --git a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs index 5d48d0e1f..a5197d6f9 100644 --- a/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs +++ b/tools/Dashboard/DevHome.Dashboard/ViewModels/WidgetViewModel.cs @@ -155,7 +155,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Warning("There was an error expanding the Widget template with data: ", ex); + _log.Warning(ex, "There was an error expanding the Widget template with data: "); ShowErrorCard("WidgetErrorCardDisplayText"); return; } @@ -192,7 +192,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Error("Error rendering widget card: ", ex); + _log.Error(ex, "Error rendering widget card: "); WidgetFrameworkElement = GetErrorCard("WidgetErrorCardDisplayText"); } }); diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml index 22512232a..c2f82f92e 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml @@ -3,6 +3,7 @@ <pg:ToolPage x:Class="DevHome.Dashboard.Views.DashboardView" + x:Uid="DashboardPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" diff --git a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs index 44c3bcafe..b42a0effa 100644 --- a/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs +++ b/tools/Dashboard/DevHome.Dashboard/Views/DashboardView.xaml.cs @@ -94,7 +94,7 @@ private async Task<bool> SubscribeToWidgetCatalogEventsAsync() } catch (Exception ex) { - _log.Error("Exception in SubscribeToWidgetCatalogEvents:", ex); + _log.Error(ex, "Exception in SubscribeToWidgetCatalogEvents:"); return false; } @@ -287,7 +287,7 @@ private async Task RestorePinnedWidgetsAsync(Widget[] hostWidgets) } catch (Exception ex) { - _log.Error($"RestorePinnedWidgets(): ", ex); + _log.Error(ex, $"RestorePinnedWidgets(): "); } } @@ -374,7 +374,7 @@ private async Task PinDefaultWidgetAsync(WidgetDefinition defaultWidgetDefinitio } catch (Exception ex) { - _log.Error($"PinDefaultWidget failed: ", ex); + _log.Error(ex, $"PinDefaultWidget failed: "); } } @@ -424,7 +424,7 @@ public async Task AddWidgetClickAsync() } catch (Exception ex) { - _log.Warning($"Creating widget failed: ", ex); + _log.Warning(ex, $"Creating widget failed: "); var mainWindow = Application.Current.GetService<WindowEx>(); var stringResource = new StringResource("DevHome.Dashboard.pri", "DevHome.Dashboard/Resources"); await mainWindow.ShowErrorMessageDialogAsync( @@ -464,7 +464,7 @@ await Task.Run(async () => { // TODO Support concurrency in dashboard. Today concurrent async execution can cause insertion errors. // https://github.com/microsoft/devhome/issues/1215 - _log.Warning($"Couldn't insert pinned widget", ex); + _log.Warning(ex, $"Couldn't insert pinned widget"); } }); } @@ -479,7 +479,7 @@ await Task.Run(async () => } catch (Exception ex) { - _log.Information($"Error deleting widget", ex); + _log.Information(ex, $"Error deleting widget"); } } }); diff --git a/tools/Environments/DevHome.Environments/DevHome.Environments.csproj b/tools/Environments/DevHome.Environments/DevHome.Environments.csproj index 3bfdf0d8a..489146ca5 100644 --- a/tools/Environments/DevHome.Environments/DevHome.Environments.csproj +++ b/tools/Environments/DevHome.Environments/DevHome.Environments.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.Environments</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Nullable>enable</Nullable> <UseWinUI>true</UseWinUI> </PropertyGroup> @@ -28,7 +28,6 @@ <Folder Include="CustomControls\" /> <Folder Include="Extensions\" /> <Folder Include="Strings\" /> - <Folder Include="ViewModels\" /> <Folder Include="Views\" /> </ItemGroup> </Project> diff --git a/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs b/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs new file mode 100644 index 000000000..c87dc219a --- /dev/null +++ b/tools/Environments/DevHome.Environments/Selectors/CardItemTemplateSelector.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Environments.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.Windows.DevHome.SDK; + +namespace DevHome.Environments.Selectors; + +public class CardItemTemplateSelector : DataTemplateSelector +{ + public DataTemplate? CreateComputeSystemOperationTemplate { get; set; } + + public DataTemplate? ComputeSystemTemplate { get; set; } + + protected override DataTemplate? SelectTemplateCore(object item) + { + return ResolveDataTemplate(item); + } + + protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container) + { + return ResolveDataTemplate(item); + } + + /// <summary> + /// Resolves the data template based on the if the ComputeSystemsListViewModel currently containers any ComputeSystemWrappers. + /// </summary> + /// <param name="item">The ComputeSystemsListViewModel object</param> + private DataTemplate? ResolveDataTemplate(object item) + { + if (item is CreateComputeSystemOperationViewModel) + { + return CreateComputeSystemOperationTemplate; + } + else + { + return ComputeSystemTemplate; + } + } +} diff --git a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw index b7f534243..67bfbae85 100644 --- a/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw +++ b/tools/Environments/DevHome.Environments/Strings/en-us/Resources.resw @@ -126,6 +126,10 @@ <value>Sync</value> <comment>Text for the sync button on the top right side</comment> </data> + <data name="CreateEnvironmentButton.Text" xml:space="preserve"> + <value>Create Environment</value> + <comment>Text for button that will redirect the user to the Create Environment page in Dev Home</comment> + </data> <data name="Titlebar.Text" xml:space="preserve"> <value>Environments</value> <comment>Title text for the main landing page</comment> @@ -190,4 +194,28 @@ <value>All</value> <comment>Text for the default value of all providers for filtering</comment> </data> + <data name="RemoveButtonTextForCreateComputeSystem" xml:space="preserve"> + <value>Remove</value> + <comment>Text for the remove button when the user wants to remove an environment being created from the UI</comment> + </data> + <data name="FailureMessageForCreateComputeSystem" xml:space="preserve"> + <value>Failed to create environment due to error: {0}</value> + <comment>Locked="{0}" Text to show the the user when Dev Home receives the notification that the creation of the new environment has failed. {0} is the error that is displayed to the user</comment> + </data> + <data name="SuccessMessageForCreateComputeSystem" xml:space="preserve"> + <value>The {0} provider has completed its creation steps. Re-sync the page to manage you're new environment</value> + <comment>Locked="{0}" Text to show the the user when Dev Home receives the notification that the users new environment has been created. {0} is the of the provider who sent the notification</comment> + </data> + <data name="CreationStatusTextForCreateEnvironmentFlow" xml:space="preserve"> + <value>Status: {0}</value> + <comment>Locked="{0}" provides the user with status message updates about a currently running operation. {0} is the status message that will appear in the UI</comment> + </data> + <data name="CreationListHeaderCreateEnvironmentFlow.Text" xml:space="preserve"> + <value>Environments being created:</value> + <comment>header text for the cards in the UI that will be displayed when they are being created</comment> + </data> + <data name="EnvironmentsToManageHeader.Text" xml:space="preserve"> + <value>Manage environments:</value> + <comment>Header text for the cards that in the UI that are not being created.</comment> + </data> </root> \ No newline at end of file diff --git a/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs b/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs index aba63a03a..4a066c0e0 100644 --- a/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs +++ b/tools/Environments/DevHome.Environments/TestModels/TestExtensionWrapper.cs @@ -12,7 +12,9 @@ namespace DevHome.Environments.TestModels; public class TestExtensionWrapper : IExtensionWrapper { - public string Name => throw new NotImplementedException(); + public string PackageDisplayName => throw new NotImplementedException(); + + public string ExtensionDisplayName => throw new NotImplementedException(); public string PackageFullName => "Microsoft.Windows.DevHome.Dev_0.0.0.0_x64__8wekyb3d8bbwe"; diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs new file mode 100644 index 000000000..7be98f72e --- /dev/null +++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemCardBase.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Environments.Models; +using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.Windows.DevHome.SDK; + +namespace DevHome.Environments.ViewModels; + +/// <summary> +/// Base class for all compute system cards that will appear in the UI. +/// </summary> +public abstract partial class ComputeSystemCardBase : ObservableObject +{ + public string Name { get; protected set; } = string.Empty; + + public string AlternativeName { get; protected set; } = string.Empty; + + public DateTime LastConnected { get; protected set; } = DateTime.Now; + + public bool IsCreateComputeSystemOperation { get; protected set; } + + // Will hold the supported actions that the user can perform on in the UI. E.g Remove button + public ObservableCollection<OperationsViewModel>? DotOperations { get; protected set; } + + [ObservableProperty] + private ComputeSystemState _state; + + [ObservableProperty] + private bool _isCreationInProgress; + + [ObservableProperty] + private CardStateColor _stateColor; + + public BitmapImage? HeaderImage { get; protected set; } = new(); + + public BitmapImage? BodyImage { get; protected set; } = new(); + + public ComputeSystem? ComputeSystem { get; protected set; } + + public string ProviderDisplayName { get; protected set; } = string.Empty; + + [ObservableProperty] + private string _uiMessageToDisplay = string.Empty; +} diff --git a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs index a411effee..1c23259c4 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/ComputeSystemViewModel.cs @@ -23,44 +23,21 @@ namespace DevHome.Environments.ViewModels; /// View model for a compute system. Each 'card' in the UI represents a compute system. /// Contains an instance of the compute system object as well. /// </summary> -public partial class ComputeSystemViewModel : ObservableObject +public partial class ComputeSystemViewModel : ComputeSystemCardBase { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ComputeSystemViewModel)); private readonly WindowEx _windowEx; - public string Name => ComputeSystem.DisplayName; - private readonly IComputeSystemManager _computeSystemManager; - public ComputeSystem ComputeSystem { get; } - - public string AlternativeName { get; } = string.Empty; - - public DateTime LastConnected { get; set; } = DateTime.Now; - - public string Type { get; } - public bool IsOperationInProgress { get; set; } // Launch button operations public ObservableCollection<OperationsViewModel> LaunchOperations { get; set; } - // Dot button operations - public ObservableCollection<OperationsViewModel> DotOperations { get; set; } - public ObservableCollection<CardProperty> Properties { get; set; } = new(); - [ObservableProperty] - private ComputeSystemState _state; - - [ObservableProperty] - private CardStateColor _stateColor; - - public BitmapImage? HeaderImage { get; set; } = new(); - - public BitmapImage? BodyImage { get; set; } = new(); - public string PackageFullName { get; set; } public ComputeSystemViewModel( @@ -74,8 +51,9 @@ public ComputeSystemViewModel( _computeSystemManager = manager; ComputeSystem = new(system); - Type = provider.DisplayName; + ProviderDisplayName = provider.DisplayName; PackageFullName = packageFullName; + Name = ComputeSystem.DisplayName; if (!string.IsNullOrEmpty(ComputeSystem.SupplementalDisplayName)) { @@ -98,7 +76,7 @@ public async Task InitializeCardDataAsync() private async Task InitializeStateAsync() { - var result = await ComputeSystem.GetStateAsync(); + var result = await ComputeSystem!.GetStateAsync(); if (result.Result.Status == ProviderOperationStatus.Failure) { _log.Error($"Failed to get state for {ComputeSystem.DisplayName} due to {result.Result.DiagnosticText}"); @@ -110,12 +88,12 @@ private async Task InitializeStateAsync() private async Task SetBodyImageAsync() { - BodyImage = await ComputeSystemHelpers.GetBitmapImageAsync(ComputeSystem); + BodyImage = await ComputeSystemHelpers.GetBitmapImageAsync(ComputeSystem!); } private async Task SetPropertiesAsync() { - foreach (var property in await ComputeSystemHelpers.GetComputeSystemPropertiesAsync(ComputeSystem, PackageFullName)) + foreach (var property in await ComputeSystemHelpers.GetComputeSystemPropertiesAsync(ComputeSystem!, PackageFullName)) { Properties.Add(property); } @@ -125,7 +103,7 @@ public void OnComputeSystemStateChanged(ComputeSystem sender, ComputeSystemState { _windowEx.DispatcherQueue.TryEnqueue(() => { - if (sender.Id == ComputeSystem.Id) + if (sender.Id == ComputeSystem!.Id) { State = state; StateColor = ComputeSystemHelpers.GetColorBasedOnState(state); @@ -135,7 +113,7 @@ public void OnComputeSystemStateChanged(ComputeSystem sender, ComputeSystemState public void RemoveStateChangedHandler() { - ComputeSystem.StateChanged -= _computeSystemManager.OnComputeSystemStateChanged; + ComputeSystem!.StateChanged -= _computeSystemManager.OnComputeSystemStateChanged; _computeSystemManager.ComputeSystemStateChanged -= OnComputeSystemStateChanged; } @@ -148,7 +126,7 @@ public void LaunchAction() Task.Run(async () => { IsOperationInProgress = true; - await ComputeSystem.ConnectAsync(string.Empty); + await ComputeSystem!.ConnectAsync(string.Empty); IsOperationInProgress = false; }); } diff --git a/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs new file mode 100644 index 000000000..d8bbf389c --- /dev/null +++ b/tools/Environments/DevHome.Environments/ViewModels/CreateComputeSystemOperationViewModel.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.Environments.Models; +using DevHome.Common.Environments.Services; +using DevHome.Common.Services; +using Microsoft.UI.Xaml.Media.Imaging; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using WinUIEx; + +namespace DevHome.Environments.ViewModels; + +/// <summary> +/// Represents a view model for the create compute system operation that will appear in the UI +/// </summary> +public partial class CreateComputeSystemOperationViewModel : ComputeSystemCardBase +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(ComputeSystemViewModel)); + + private readonly IComputeSystemManager _computeSystemManager; + + private readonly WindowEx _windowEx; + + private readonly StringResource _stringResource; + + /// <summary> + /// This glyph can be found in the Fluent UI MDL2 Assets font that ships with Windows. + /// Check the Win UI 3 gallery for the visual representation of the glyph. It is + /// the trash can icon. + /// </summary> + private readonly string _cancelationUniCodeForGlyph = "\uE74D"; + + public string EnvironmentName => Operation.EnvironmentName; + + /// <summary> + /// Callback action to remove the view model from the view. + /// </summary> + private readonly Func<ComputeSystemCardBase, bool> _removalAction; + + public CreateComputeSystemOperation Operation { get; } + + public CreateComputeSystemOperationViewModel( + IComputeSystemManager computeSystemManager, + StringResource stringResource, + WindowEx windowsEx, + Func<ComputeSystemCardBase, bool> removalAction, + CreateComputeSystemOperation operation) + { + IsCreationInProgress = true; + _windowEx = windowsEx; + _removalAction = removalAction; + _stringResource = stringResource; + _computeSystemManager = computeSystemManager; + Operation = operation; + + var providerDetails = Operation.ProviderDetails; + ProviderDisplayName = providerDetails.ComputeSystemProvider.DisplayName; + IsCreateComputeSystemOperation = true; + + // Hook up event handlers to the operation + Operation.Completed += OnOperationCompleted; + Operation.Progress += OnOperationProgressChanged; + + // make sure the last update appears in the UI if the operation is already completed at this point + UpdateUiMessage(Operation.LastProgressMessage, Operation.LastProgressPercentage); + + // Update the state of the card + State = ComputeSystemState.Creating; + StateColor = CardStateColor.Caution; + + // Setup the button to remove the the view model from the UI and the header Image + DotOperations = new ObservableCollection<OperationsViewModel>() { new(_stringResource.GetLocalized("RemoveButtonTextForCreateComputeSystem"), _cancelationUniCodeForGlyph, RemoveViewModelFromUI) }; + HeaderImage = CardProperty.ConvertMsResourceToIcon(providerDetails.ComputeSystemProvider.Icon, providerDetails.ExtensionWrapper.PackageFullName); + + // If the operation is already completed update the status + if (operation.CreateComputeSystemResult != null) + { + UpdateStatusIfCompleted(operation.CreateComputeSystemResult); + } + } + + private void OnOperationCompleted(object sender, CreateComputeSystemResult createComputeSystemResult) + { + UpdateStatusIfCompleted(createComputeSystemResult); + } + + private void UpdateStatusIfCompleted(CreateComputeSystemResult createComputeSystemResult) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + // Update the creation status + IsCreationInProgress = false; + if (createComputeSystemResult.Result.Status == ProviderOperationStatus.Success) + { + UpdateUiMessage(_stringResource.GetLocalized("SuccessMessageForCreateComputeSystem", ProviderDisplayName)); + ComputeSystem = new(createComputeSystemResult.ComputeSystem); + State = ComputeSystemState.Created; + StateColor = CardStateColor.Success; + } + else + { + UpdateUiMessage(_stringResource.GetLocalized("FailureMessageForCreateComputeSystem", createComputeSystemResult.Result.DisplayMessage)); + State = ComputeSystemState.Unknown; + StateColor = CardStateColor.Failure; + } + + RemoveEventHandlers(); + }); + } + + private void OnOperationProgressChanged(object sender, CreateComputeSystemProgressEventArgs args) + { + UpdateUiMessage(args.Status, args.PercentageCompleted); + } + + public void RemoveEventHandlers() + { + Operation.Completed -= OnOperationCompleted; + Operation.Progress -= OnOperationProgressChanged; + } + + private void UpdateUiMessage(string operationStatus, uint percentage = 0) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + if (operationStatus == null) + { + return; + } + + var percentageString = percentage == 0 ? string.Empty : $"({percentage}%)"; + UiMessageToDisplay = _stringResource.GetLocalized("CreationStatusTextForCreateEnvironmentFlow", $"{operationStatus} {percentageString}"); + }); + } + + private void RemoveViewModelFromUI() + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + _removalAction(this); + RemoveEventHandlers(); + Operation.CancelOperation(); + _computeSystemManager.RemoveOperation(Operation); + }); + } +} diff --git a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs index ce8a84fad..3b0d268cf 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/LandingPageViewModel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; @@ -36,15 +37,19 @@ public partial class LandingPageViewModel : ObservableObject, IDisposable private readonly IComputeSystemManager _computeSystemManager; + private readonly INavigationService _navigationService; + private readonly StringResource _stringResource; private readonly object _lock = new(); + private bool _wasSyncButtonClicked; + public bool IsLoading { get; set; } - public ObservableCollection<ComputeSystemViewModel> ComputeSystems { get; set; } = new(); + public ObservableCollection<ComputeSystemCardBase> ComputeSystemCards { get; set; } = new(); - public AdvancedCollectionView ComputeSystemsView { get; set; } + public AdvancedCollectionView ComputeSystemCardsView { get; set; } public bool HasPageLoadedForTheFirstTime { get; set; } @@ -60,11 +65,15 @@ public partial class LandingPageViewModel : ObservableObject, IDisposable [ObservableProperty] private string _lastSyncTime; + [ObservableProperty] + private bool _shouldShowCreationHeader; + public ObservableCollection<string> Providers { get; set; } private CancellationTokenSource _cancellationTokenSource = new(); public LandingPageViewModel( + INavigationService navigationService, IComputeSystemManager manager, EnvironmentsExtensionsService extensionsService, NotificationService notificationService, @@ -74,6 +83,7 @@ public LandingPageViewModel( _extensionsService = extensionsService; _notificationService = notificationService; _windowEx = windowEx; + _navigationService = navigationService; _stringResource = new StringResource("DevHome.Environments.pri", "DevHome.Environments/Resources"); @@ -81,7 +91,7 @@ public LandingPageViewModel( Providers = new() { _stringResource.GetLocalized("AllProviders") }; _lastSyncTime = _stringResource.GetLocalized("MomentsAgo"); - ComputeSystemsView = new AdvancedCollectionView(ComputeSystems); + ComputeSystemCardsView = new AdvancedCollectionView(ComputeSystemCards); } public void Initialize(StackedNotificationsBehavior notificationQueue) @@ -99,18 +109,28 @@ public async Task SyncButton() SelectedSortIndex = -1; Providers = new ObservableCollection<string> { _stringResource.GetLocalized("AllProviders") }; SelectedProviderIndex = 0; + _wasSyncButtonClicked = true; // Reset the old sync timer _cancellationTokenSource.Cancel(); await _windowEx.DispatcherQueue.EnqueueAsync(() => LastSyncTime = _stringResource.GetLocalized("MomentsAgo")); + // We need to signal to the compute system manager that it can remove all the completed operations now that + // we're done showing them in the view. + _computeSystemManager.RemoveAllCompletedOperations(); await LoadModelAsync(); + _wasSyncButtonClicked = false; + } - // Start a new sync timer - _ = Task.Run(async () => - { - await RunSyncTimmer(); - }); + /// <summary> + /// Navigates the user to the select environments page in the setup flow. This is the first page in the create environment + /// process. + /// </summary> + [RelayCommand] + public void CreateEnvironmentButton() + { + _log.Information("User clicked on the create environment button. Navigating to Select environment page in Setup flow"); + _navigationService.NavigateTo(KnownPageKeys.SetupFlow, "startCreationFlow"); } // Updates the last sync time on the UI thread after set delay @@ -171,20 +191,25 @@ public async Task LoadModelAsync(bool useDebugValues = false) return; } - HasPageLoadedForTheFirstTime = true; + // If the page has already loaded once, then we don't need to re-load the compute systems as that can take a while. + // The user can click the sync button to refresh the compute systems. However, there may be new operations that have started + // since the last time the page was loaded. So we need to add those to the view model quickly. + SetupCreateComputeSystemOperationForUI(); + if (HasPageLoadedForTheFirstTime && !_wasSyncButtonClicked) + { + return; + } + IsLoading = true; } - // Start a new sync timer - _ = Task.Run(async () => + for (var i = ComputeSystemCards.Count - 1; i >= 0; i--) { - await RunSyncTimmer(); - }); - - for (var i = ComputeSystems.Count - 1; i >= 0; i--) - { - ComputeSystems[i].RemoveStateChangedHandler(); - ComputeSystems.RemoveAt(i); + if (ComputeSystemCards[i] is ComputeSystemViewModel computeSystemViewModel) + { + computeSystemViewModel.RemoveStateChangedHandler(); + ComputeSystemCards.RemoveAt(i); + } } ShowLoadingShimmer = true; @@ -194,6 +219,34 @@ public async Task LoadModelAsync(bool useDebugValues = false) lock (_lock) { IsLoading = false; + HasPageLoadedForTheFirstTime = true; + } + } + + /// <summary> + /// Sets up the view model to show the create compute system operations that the compute system manager contains. + /// </summary> + private void SetupCreateComputeSystemOperationForUI() + { + // Remove all the operations from view and then add the ones the manager has. + _log.Information($"Adding any new create compute system operations to ComputeSystemCards list"); + var curOperations = _computeSystemManager.GetRunningOperationsForCreation(); + for (var i = ComputeSystemCards.Count - 1; i >= 0; i--) + { + if (ComputeSystemCards[i].IsCreateComputeSystemOperation) + { + var operationViewModel = ComputeSystemCards[i] as CreateComputeSystemOperationViewModel; + operationViewModel!.RemoveEventHandlers(); + ComputeSystemCards.RemoveAt(i); + } + } + + // Add new operations to the list + foreach (var operation in curOperations) + { + // this is a new operation so we need to create a view model for it. + ComputeSystemCards.Add(new CreateComputeSystemOperationViewModel(_computeSystemManager, _stringResource, _windowEx, ComputeSystemCards.Remove, operation)); + _log.Information($"Found new create compute system operation for provider {operation.ProviderDetails.ComputeSystemProvider}, with name {operation.EnvironmentName}"); } } @@ -208,7 +261,7 @@ private async Task AddAllComputeSystemsFromAProvider(ComputeSystemsLoadedData da var result = mapping.Value.Result; await _notificationService.ShowNotificationAsync(provider.DisplayName, result.DisplayMessage, InfoBarSeverity.Error); - _log.Error($"Error occurred while adding Compute systems to environments page for provider: {provider.Id}", result.DiagnosticText, result.ExtendedError); + _log.Error($"Error occurred while adding Compute systems to environments page for provider: {provider.Id}. {result.DiagnosticText}, {result.ExtendedError}"); data.DevIdToComputeSystemMap.Remove(mapping.Key); } @@ -237,12 +290,13 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => packageFullName, _windowEx); await computeSystemViewModel.InitializeCardDataAsync(); - ComputeSystems.Add(computeSystemViewModel); + + ComputeSystemCards.Add(computeSystemViewModel); } } catch (Exception ex) { - _log.Error($"Exception occurred while adding Compute systems to environments page for provider: {provider.Id}", ex); + _log.Error(ex, $"Exception occurred while adding Compute systems to environments page for provider: {provider.Id}"); } }); } @@ -253,11 +307,16 @@ await _windowEx.DispatcherQueue.EnqueueAsync(async () => [RelayCommand] public void SearchHandler(string query) { - ComputeSystemsView.Filter = system => + ComputeSystemCardsView.Filter = system => { + if (system is CreateComputeSystemOperationViewModel createComputeSystemOperationViewModel) + { + return createComputeSystemOperationViewModel.EnvironmentName.Contains(query, StringComparison.OrdinalIgnoreCase); + } + if (system is ComputeSystemViewModel computeSystemViewModel) { - var systemName = computeSystemViewModel.ComputeSystem.DisplayName; + var systemName = computeSystemViewModel.ComputeSystem!.DisplayName; var systemAltName = computeSystemViewModel.ComputeSystem.SupplementalDisplayName; return systemName.Contains(query, StringComparison.OrdinalIgnoreCase) || systemAltName.Contains(query, StringComparison.OrdinalIgnoreCase); } @@ -270,20 +329,25 @@ public void SearchHandler(string query) /// Updates the view model to filter the compute systems according to the provider. /// </summary> [RelayCommand] - public void ProviderHandler() + public void ProviderHandler(int selectedIndex) { + SelectedProviderIndex = selectedIndex; var currentProvider = Providers[SelectedProviderIndex]; - ComputeSystemsView.Filter = system => + ComputeSystemCardsView.Filter = system => { if (currentProvider.Equals(_stringResource.GetLocalized("AllProviders"), StringComparison.OrdinalIgnoreCase)) { return true; } + if (system is CreateComputeSystemOperationViewModel createComputeSystemOperationViewModel) + { + return createComputeSystemOperationViewModel.ProviderDisplayName.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); + } + if (system is ComputeSystemViewModel computeSystemViewModel) { - var type = computeSystemViewModel.Type; - return type.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); + return computeSystemViewModel.ProviderDisplayName.Equals(currentProvider, StringComparison.OrdinalIgnoreCase); } return false; @@ -293,27 +357,30 @@ public void ProviderHandler() /// <summary> /// Updates the view model to sort the compute systems according to the sort criteria. /// </summary> + /// <remarks> + /// New SortDescription property names should be added as new properties to <see cref="ComputeSystemCardBase"/> + /// </remarks> [RelayCommand] public void SortHandler() { - ComputeSystemsView.SortDescriptions.Clear(); + ComputeSystemCardsView.SortDescriptions.Clear(); switch (SelectedSortIndex) { case 0: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Ascending)); break; case 1: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Descending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("Name", SortDirection.Descending)); break; case 2: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Ascending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Ascending)); break; case 3: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Descending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("AlternativeName", SortDirection.Descending)); break; case 4: - ComputeSystemsView.SortDescriptions.Add(new SortDescription("LastConnected", SortDirection.Ascending)); + ComputeSystemCardsView.SortDescriptions.Add(new SortDescription("LastConnected", SortDirection.Ascending)); break; } } diff --git a/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs b/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs index 4b87fa35f..32d002d68 100644 --- a/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs +++ b/tools/Environments/DevHome.Environments/ViewModels/OperationsViewModel.cs @@ -8,25 +8,44 @@ namespace DevHome.Environments.ViewModels; +public enum OperationKind +{ + ExtensionTask, + DevHomeAction, +} + /// <summary> /// Represents an operation that can be performed on a compute system. /// This is used to populate the launch and dot buttons on the compute system card. /// </summary> public partial class OperationsViewModel { + private readonly OperationKind _operationKind; + public string Name { get; } public string? Description { get; set; } public string IconGlyph { get; } - private Func<string, Task<ComputeSystemOperationResult>> Command { get; } + private Func<string, Task<ComputeSystemOperationResult>>? ExtensionTask { get; } + + private Action? DevHomeAction { get; } public OperationsViewModel(string name, string icon, Func<string, Task<ComputeSystemOperationResult>> command) { + _operationKind = OperationKind.ExtensionTask; Name = name; IconGlyph = icon; - Command = command; + ExtensionTask = command; + } + + public OperationsViewModel(string name, string icon, Action command) + { + _operationKind = OperationKind.DevHomeAction; + Name = name; + IconGlyph = icon; + DevHomeAction = command; } [RelayCommand] @@ -35,7 +54,14 @@ public void InvokeAction() // We'll need to disable the card UI while the operation is in progress and handle failures. Task.Run(async () => { - await Command(string.Empty); + if (_operationKind == OperationKind.DevHomeAction) + { + DevHomeAction!(); + return; + } + + // We'll need to handle the case where the DevHome service is not available. + await ExtensionTask!(string.Empty); }); } } diff --git a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml index 4b545bab9..910e6d6cb 100644 --- a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml +++ b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml @@ -8,13 +8,14 @@ xmlns:customControls="using:DevHome.Environments.CustomControls" xmlns:commonCustomControls="using:DevHome.Common.Environments.CustomControls" xmlns:commonModels="using:DevHome.Common.Environments.Models" + xmlns:selectors="using:DevHome.Environments.Selectors" xmlns:i="using:Microsoft.Xaml.Interactivity" xmlns:ic="using:Microsoft.Xaml.Interactions.Core" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:winUIBehaviors="using:CommunityToolkit.WinUI.Behaviors" + xmlns:converters="using:CommunityToolkit.WinUI.Converters" behaviors:NavigationViewHeaderBehavior.HeaderMode="Never" Loaded="OnLoaded"> - <pg:ToolPage.Resources> <ResourceDictionary Source="ms-appx:///DevHome.Common/Environments/Templates/EnvironmentsTemplates.xaml" /> </pg:ToolPage.Resources> @@ -30,6 +31,8 @@ <!-- Adding unshared resources/templates here--> <Grid.Resources> + <converters:EmptyCollectionToObjectConverter x:Key="EmptyCollectionVisibilityConverter" EmptyValue="Collapsed" NotEmptyValue="Visible"/> + <!-- Launch Button template --> <DataTemplate x:Key="LaunchButton" x:DataType="vm:ComputeSystemViewModel"> <SplitButton @@ -55,6 +58,18 @@ </Grid> </DataTemplate> + <!-- Three Dot Button For create compute system operation template --> + <DataTemplate x:Key="ThreeDotsButtonForCreation" x:DataType="vm:CreateComputeSystemOperationViewModel"> + <Grid> + <Button + Style="{StaticResource HorizontalThreeDotsStyle}"> + <Button.Flyout> + <customControls:CardFlyout ItemsViewModels="{x:Bind DotOperations}"/> + </Button.Flyout> + </Button> + </Grid> + </DataTemplate> + <!-- Properties template for the compute system properties that appear within a horizontal card.--> <DataTemplate x:Key="BottomRowProperties" x:DataType="commonModels:CardProperty"> <Grid @@ -108,6 +123,63 @@ </ListView> </DataTemplate> + <DataTemplate x:Key="ComputeSystemTemplate" x:DataType="vm:ComputeSystemViewModel"> + <Grid Style="{StaticResource HorizontalCardRootForEnvironmentsPage}"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <!-- Card Header : Title, small icon, '...' button --> + <Grid Grid.Row="0"> + <commonCustomControls:CardHeader + HeaderCaption="{x:Bind ProviderDisplayName, Mode=OneWay}" + HeaderIcon="{x:Bind HeaderImage, Mode=OneWay}" + ActionControlTemplate="{StaticResource ThreeDotsButton}" /> + </Grid> + <!-- Card Body : Name, thumbnail, 'Launch' button --> + <Grid Grid.Row="1"> + <commonCustomControls:CardBody + ComputeSystemTitle="{x:Bind Name, Mode=OneWay}" + ComputeSystemAlternativeTitle="{x:Bind AlternativeName, Mode=OneWay}" + ComputeSystemImage="{x:Bind BodyImage, Mode=OneWay}" + CardState="{x:Bind State, Mode=OneWay}" + StateColor="{x:Bind StateColor, Mode=OneWay}" + ComputeSystemProperties="{x:Bind Properties, Mode=OneWay}" + ComputeSystemPropertyTemplate="{StaticResource BottomRowProperties}" + ActionControlTemplate="{StaticResource LaunchButton}"/> + </Grid> + </Grid> + </DataTemplate> + + <DataTemplate x:Key="CreateComputeSystemOperationTemplate" x:DataType="vm:CreateComputeSystemOperationViewModel"> + <Grid Style="{StaticResource HorizontalCardRootForEnvironmentsPage}"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + <!-- Card Header : Title, small icon, '...' button --> + <Grid Grid.Row="0"> + <commonCustomControls:CardHeader + HeaderCaption="{x:Bind ProviderDisplayName, Mode=OneWay}" + HeaderIcon="{x:Bind HeaderImage, Mode=OneWay}" + ActionControlTemplate="{StaticResource ThreeDotsButtonForCreation}" /> + </Grid> + <Grid Grid.Row="1"> + <commonCustomControls:CardBody + ComputeSystemTitle="{x:Bind EnvironmentName, Mode=OneWay}" + StateColor="{x:Bind StateColor, Mode=OneWay}" + ComputeSystemCreationStatus="{x:Bind UiMessageToDisplay, Mode=OneWay}" + ShouldShowInDefiniteProgress="{x:Bind IsCreationInProgress, Mode=OneWay}"/> + </Grid> + </Grid> + </DataTemplate> + + <selectors:CardItemTemplateSelector x:Key="CardItemTemplateSelector" + ComputeSystemTemplate="{StaticResource ComputeSystemTemplate}" + CreateComputeSystemOperationTemplate="{StaticResource CreateComputeSystemOperationTemplate}"/> + </Grid.Resources> <!-- Templates end here --> @@ -118,8 +190,23 @@ <ColumnDefinition Width="auto" /> <ColumnDefinition Width="auto" /> <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" x:Uid="Titlebar" Style="{ThemeResource SubtitleTextBlockStyle}" HorizontalAlignment="Left" x:Name="Titlebar"/> + <Button + Grid.Column="3" + Style="{ThemeResource AccentButtonStyle}" + HorizontalAlignment="Right" + Command="{x:Bind ViewModel.CreateEnvironmentButtonCommand}" > + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="auto" /> + <ColumnDefinition Width="*" /> + </Grid.ColumnDefinitions> + <FontIcon Grid.Column="0" Glyph=""/> + <TextBlock Grid.Column="1" x:Uid="CreateEnvironmentButton" Margin="8 0 0 0"/> + </Grid> + </Button> </Grid> <Grid Grid.Row="1" MaxWidth="{ThemeResource MaxPageContentWidth}" Margin="{ThemeResource ContentPageMargin}"> @@ -142,11 +229,14 @@ <!-- Provider field --> <TextBlock x:Uid="ProviderTextBlock" VerticalAlignment="Center" Margin="50 0 5 0"/> <ComboBox x:Uid="ProviderSelectionComboBox" x:Name="ProviderSelectionComboBox" Margin="0 3 5 0" - ItemsSource="{x:Bind ViewModel.Providers}" SelectedIndex="{x:Bind ViewModel.SelectedProviderIndex, Mode=TwoWay}"> + ItemsSource="{x:Bind ViewModel.Providers, Mode=OneWay}" + SelectedIndex="{x:Bind ViewModel.SelectedProviderIndex, Mode=TwoWay}"> <i:Interaction.Behaviors> <ic:EventTriggerBehavior EventName="SelectionChanged"> - <ic:InvokeCommandAction Command="{x:Bind ViewModel.ProviderHandlerCommand}" /> - </ic:EventTriggerBehavior> + <ic:InvokeCommandAction + Command="{x:Bind ViewModel.ProviderHandlerCommand}" + CommandParameter="{Binding SelectedIndex, ElementName=ProviderSelectionComboBox, Mode=OneWay}" /> + </ic:EventTriggerBehavior> </i:Interaction.Behaviors> </ComboBox> <!-- Sort field --> @@ -191,44 +281,15 @@ <ScrollViewer Grid.Row="3" Style="{StaticResource EnvironmentScrollViewerStyle}" MaxWidth="{ThemeResource MaxPageContentWidth}" Margin="{ThemeResource ContentPageMargin}"> <StackPanel> <ListView - MaxWidth="{ThemeResource MaxPageContentWidth}" - ItemsSource="{x:Bind ViewModel.ComputeSystemsView}" SelectionMode="None" - ItemContainerStyle="{StaticResource HorizontalCardListViewItemContainerForManagementPageStyle}"> + MaxWidth="{ThemeResource MaxPageContentWidth}" + ItemsSource="{x:Bind ViewModel.ComputeSystemCardsView}" SelectionMode="None" + ItemTemplateSelector="{StaticResource CardItemTemplateSelector}" + ItemContainerStyle="{StaticResource HorizontalCardListViewItemContainerForManagementPageStyle}"> <ListView.ItemsPanel> <ItemsPanelTemplate> <StackPanel></StackPanel> </ItemsPanelTemplate> </ListView.ItemsPanel> - <ListView.ItemTemplate> - <DataTemplate x:DataType="vm:ComputeSystemViewModel"> - <Grid Style="{StaticResource HorizontalCardRootForEnvironmentsPage}"> - <Grid.RowDefinitions> - <RowDefinition Height="Auto" /> - <RowDefinition Height="Auto" /> - <RowDefinition Height="*" /> - </Grid.RowDefinitions> - <!-- Card Header : Title, small icon, '...' button --> - <Grid Grid.Row="0"> - <commonCustomControls:CardHeader - HeaderCaption="{x:Bind Type, Mode=OneWay}" - HeaderIcon="{x:Bind HeaderImage, Mode=OneWay}" - ActionControlTemplate="{StaticResource ThreeDotsButton}" /> - </Grid> - <!-- Card Body : Name, thumbnail, 'Launch' button --> - <Grid Grid.Row="1"> - <commonCustomControls:CardBody - ComputeSystemTitle="{x:Bind Name, Mode=OneWay}" - ComputeSystemAlternativeTitle="{x:Bind AlternativeName, Mode=OneWay}" - ComputeSystemImage="{x:Bind BodyImage, Mode=OneWay}" - CardState="{x:Bind State, Mode=OneWay}" - StateColor="{x:Bind StateColor, Mode=OneWay}" - ComputeSystemProperties="{x:Bind Properties, Mode=OneWay}" - ComputeSystemPropertyTemplate="{StaticResource BottomRowProperties}" - ActionControlTemplate="{StaticResource LaunchButton}"/> - </Grid> - </Grid> - </DataTemplate> - </ListView.ItemTemplate> </ListView> <StackPanel Visibility="{x:Bind ViewModel.ShowLoadingShimmer, Mode=OneWay}"> <ContentControl diff --git a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml.cs b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml.cs index c814b3049..10c2fca29 100644 --- a/tools/Environments/DevHome.Environments/Views/LandingPage.xaml.cs +++ b/tools/Environments/DevHome.Environments/Views/LandingPage.xaml.cs @@ -54,11 +54,6 @@ private async Task LocalLoadButton() private void OnLoaded(object sender, RoutedEventArgs e) { - if (ViewModel.HasPageLoadedForTheFirstTime) - { - return; - } - _ = ViewModel.LoadModelAsync(false); } } diff --git a/tools/Experiments/src/DevHome.Experiments.csproj b/tools/Experiments/src/DevHome.Experiments.csproj index 64f95fc6a..d613eeb4a 100644 --- a/tools/Experiments/src/DevHome.Experiments.csproj +++ b/tools/Experiments/src/DevHome.Experiments.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.Experiments</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <UseWinUI>true</UseWinUI> </PropertyGroup> <ItemGroup> diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj index b1c4b5bae..79a64f896 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/DevHome.ExtensionLibrary.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.ExtensionLibrary</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Nullable>enable</Nullable> <UseWinUI>true</UseWinUI> </PropertyGroup> diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs index 53b24f863..f9a50252c 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionLibraryViewModel.cs @@ -79,7 +79,7 @@ private async Task GetInstalledExtensionsAsync() InstalledPackagesList.Clear(); - extensionWrappers = extensionWrappers.OrderBy(extensionWrapper => extensionWrapper.Name); + extensionWrappers = extensionWrappers.OrderBy(extensionWrapper => extensionWrapper.PackageDisplayName); foreach (var extensionWrapper in extensionWrappers) { @@ -90,7 +90,7 @@ private async Task GetInstalledExtensionsAsync() } var hasSettingsProvider = extensionWrapper.HasProviderType(ProviderType.Settings); - var extension = new InstalledExtensionViewModel(extensionWrapper.Name, extensionWrapper.ExtensionUniqueId, hasSettingsProvider); + var extension = new InstalledExtensionViewModel(extensionWrapper.ExtensionDisplayName, extensionWrapper.ExtensionUniqueId, hasSettingsProvider); // Each extension is shown under the package that contains it. Check if we have the package in the list // already and if not, create it and add it to the list of packages. Then add the extension to that @@ -99,7 +99,7 @@ private async Task GetInstalledExtensionsAsync() if (package == null) { package = new InstalledPackageViewModel( - extensionWrapper.Name, + extensionWrapper.PackageDisplayName, extensionWrapper.Publisher, extensionWrapper.PackageFamilyName, extensionWrapper.InstalledDate, @@ -124,7 +124,7 @@ private async Task<string> GetStoreData() } catch (Exception ex) { - _log.Error("Error retrieving packages", ex); + _log.Error(ex, "Error retrieving packages"); ShouldShowStoreError = true; } diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs index cb1aac5ad..3a4cb4710 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/ExtensionSettingsViewModel.cs @@ -46,7 +46,7 @@ private async Task OnSettingsContentLoadedAsync(ExtensionAdaptiveCardPanel exten if ((_navigationService.LastParameterUsed != null) && ((string)_navigationService.LastParameterUsed == extensionWrapper.ExtensionUniqueId)) { - FillBreadcrumbBar(extensionWrapper.Name); + FillBreadcrumbBar(extensionWrapper.ExtensionDisplayName); var settingsProvider = Task.Run(() => extensionWrapper.GetProviderAsync<ISettingsProvider>()).Result; if (settingsProvider != null) diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs index 3232666da..7593a2ac7 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/ViewModels/InstalledPackageViewModel.cs @@ -87,7 +87,7 @@ private void NavigateSettings() public partial class InstalledPackageViewModel : ObservableObject { [ObservableProperty] - private string _title; + private string _displayName; [ObservableProperty] private string _publisher; @@ -103,9 +103,9 @@ public partial class InstalledPackageViewModel : ObservableObject public ObservableCollection<InstalledExtensionViewModel> InstalledExtensionsList { get; set; } - public InstalledPackageViewModel(string title, string publisher, string packageFamilyName, DateTimeOffset installedDate, PackageVersion version) + public InstalledPackageViewModel(string displayName, string publisher, string packageFamilyName, DateTimeOffset installedDate, PackageVersion version) { - _title = title; + _displayName = displayName; _publisher = publisher; _packageFamilyName = packageFamilyName; _installedDate = installedDate; diff --git a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml index c14d73765..5f56915dd 100644 --- a/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml +++ b/tools/ExtensionLibrary/DevHome.ExtensionLibrary/Views/ExtensionLibraryView.xaml @@ -62,7 +62,7 @@ <ItemsRepeater ItemsSource="{x:Bind ViewModel.InstalledPackagesList, Mode=OneWay}"> <ItemsRepeater.ItemTemplate> <DataTemplate x:DataType="viewmodels:InstalledPackageViewModel"> - <ctControls:SettingsExpander Header="{x:Bind Title}" + <ctControls:SettingsExpander Header="{x:Bind DisplayName}" Description="{x:Bind GeneratePackageDetails(Version,Publisher , InstalledDate), Mode=OneWay}" Margin="{ThemeResource SettingsCardMargin}" ItemsSource="{x:Bind InstalledExtensionsList}" @@ -70,24 +70,27 @@ <ctControls:SettingsExpander.HeaderIcon> <FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph=""/> </ctControls:SettingsExpander.HeaderIcon> - <Button x:Uid="MoreOptionsButton" - Content="" - Height="36" Width="36" - FontFamily="{StaticResource SymbolThemeFontFamily}" - Background="Transparent" - BorderThickness="0"> - <Button.Flyout> - <MenuFlyout> - <MenuFlyoutItem - x:Uid="UninstallItem" - Command="{x:Bind UninstallButtonCommand}"> - </MenuFlyoutItem> - </MenuFlyout> - </Button.Flyout> - </Button> + <StackPanel> + <!-- This StackPanel is a workaround for the bug https://github.com/CommunityToolkit/Windows/issues/396 --> + <Button x:Uid="MoreOptionsButton" + Content="" + Height="36" Width="36" + FontFamily="{StaticResource SymbolThemeFontFamily}" + Background="Transparent" + BorderThickness="0"> + <Button.Flyout> + <MenuFlyout> + <MenuFlyoutItem + x:Uid="UninstallItem" + Command="{x:Bind UninstallButtonCommand}"> + </MenuFlyoutItem> + </MenuFlyout> + </Button.Flyout> + </Button> + </StackPanel> <ctControls:SettingsExpander.ItemTemplate> <DataTemplate x:DataType="viewmodels:InstalledExtensionViewModel"> - <ctControls:SettingsCard x:Uid="ManageExtensionCard" + <ctControls:SettingsCard Header="{x:Bind DisplayName}" CornerRadius="0,0,3,3" IsClickEnabled="{x:Bind HasSettingsProvider}" Command="{x:Bind NavigateSettingsCommand}"> @@ -116,6 +119,7 @@ Description="{x:Bind Publisher}" Margin="{ThemeResource SettingsCardMargin}" CornerRadius="3" + AutomationProperties.Name="{x:Bind Title}" IsClickEnabled="False"> <ctControls:SettingsCard.HeaderIcon> <FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph=""/> diff --git a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj index 4438bed66..a66c040a1 100644 --- a/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj +++ b/tools/QuietBackgroundProcesses/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection/DevHome.QuietBackgroundProcesses.ElevatedServer.Projection.csproj @@ -6,6 +6,8 @@ <PropertyGroup> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> + <Platforms>x86;x64;arm64</Platforms> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <MdmergeMergedDir>$(CppBaseOutDir)\DevHome.QuietBackgroundProcesses.Common\</MdmergeMergedDir> </PropertyGroup> diff --git a/tools/SampleTool/src/SampleTool.csproj b/tools/SampleTool/src/SampleTool.csproj index a966bc9cf..dab48d8ee 100644 --- a/tools/SampleTool/src/SampleTool.csproj +++ b/tools/SampleTool/src/SampleTool.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>SampleTool</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <UseWinUI>true</UseWinUI> </PropertyGroup> <ItemGroup> diff --git a/tools/SampleTool/unittest/SampleTool.UnitTest.csproj b/tools/SampleTool/unittest/SampleTool.UnitTest.csproj index 11cc24d0d..f71eb66bf 100644 --- a/tools/SampleTool/unittest/SampleTool.UnitTest.csproj +++ b/tools/SampleTool/unittest/SampleTool.UnitTest.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>SampleTool.Test</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <IsPackable>false</IsPackable> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs index e8f8027d7..f5d356cfb 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/DevDriveFormatter/DevDriveFormatter.cs @@ -88,7 +88,7 @@ public int FormatPartitionAsDevDrive(char curDriveLetter, string driveLabel) } catch (CimException e) { - _log.Error($"A CimException occurred while formatting Dev Drive Error.", e); + _log.Error(e, $"A CimException occurred while formatting Dev Drive Error."); return e.HResult; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj b/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj index 21153fbde..33ab51ab0 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/DevHome.SetupFlow.Common.csproj @@ -3,8 +3,9 @@ <PropertyGroup> <RootNamespace>DevHome.SetupFlow.Common</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Nullable>disable</Nullable> + <ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles> </PropertyGroup> <!-- diff --git a/tools/SetupFlow/DevHome.SetupFlow.Common/Elevation/IPCSetup.cs b/tools/SetupFlow/DevHome.SetupFlow.Common/Elevation/IPCSetup.cs index 602d3f250..d7acd95fd 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.Common/Elevation/IPCSetup.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.Common/Elevation/IPCSetup.cs @@ -296,7 +296,7 @@ public static (RemoteObject<T>, Process) CreateOutOfProcessObjectAndGetProcess<T } catch (Exception e) { - Log.Error($"Error occurring while setting up elevated process:", e); + Log.Error(e, $"Error occurring while setting up elevated process:"); // Release the "mutex" if there is any error. // On success, the mutex will be released after work is done. @@ -386,7 +386,7 @@ public static void CompleteRemoteObjectInitialization<T>( } catch (Exception e) { - Log.Error($"Error occurred during setup.", e); + Log.Error(e, $"Error occurred during setup."); mappedMemory.HResult = e.HResult; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent.Projection/DevHome.SetupFlow.ElevatedComponent.Projection.csproj b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent.Projection/DevHome.SetupFlow.ElevatedComponent.Projection.csproj index 78272f481..a9f2db7b7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent.Projection/DevHome.SetupFlow.ElevatedComponent.Projection.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent.Projection/DevHome.SetupFlow.ElevatedComponent.Projection.csproj @@ -7,6 +7,8 @@ <PropertyGroup> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> + <Platforms>x86;x64;arm64</Platforms> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> </PropertyGroup> <!-- CsWinRT properties --> diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/DevHome.SetupFlow.ElevatedComponent.csproj b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/DevHome.SetupFlow.ElevatedComponent.csproj index f5b24448f..fa4f3a6eb 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/DevHome.SetupFlow.ElevatedComponent.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/DevHome.SetupFlow.ElevatedComponent.csproj @@ -6,6 +6,8 @@ <PropertyGroup> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> + <Platforms>x86;x64;arm64</Platforms> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> </PropertyGroup> <!-- CsWinRT properties --> @@ -29,11 +31,11 @@ <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> - <!-- + <!-- The ExcludeAssets=runtime is needed to prevent build errors from System.Management.dll being produced in 2 different locations. DevHome.SetupFlow.Common.csproj, uses System.Management instead, to prevent the reference from escaping into here and producing a separate dll that will break the build, this package reference overwrites the one inherited from the comInterop the project. - The ComInterop project was used since this The ElevatedComponent already depends on it. But a better solution is needed once we figure out + The ComInterop project was used since this The ElevatedComponent already depends on it. But a better solution is needed once we figure out why 2 separate dlls get produced. --> </ItemGroup> diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs index ff4a8fcd5..9e848df2d 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/ElevatedComponentOperation.cs @@ -43,7 +43,7 @@ public ElevatedComponentOperation(IList<string> tasksArgumentList) } catch (Exception e) { - _log.Error($"Failed to parse tasks arguments", e); + _log.Error(e, $"Failed to parse tasks arguments"); throw; } } @@ -182,7 +182,7 @@ private async Task<TResult> ValidateAndExecuteAsync<TTaskArguments, TResult>( } catch (Exception e) { - _log.Error($"Failed to validate or execute operation", e); + _log.Error(e, $"Failed to validate or execute operation"); EndOperation(taskArguments, false); throw; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs index 4238de1c5..bad73c337 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedConfigurationTask.cs @@ -56,7 +56,7 @@ public IAsyncOperation<ElevatedConfigureTaskResult> ApplyConfiguration(string fi } catch (Exception e) { - log.Error($"Failed to apply configuration.", e); + log.Error(e, $"Failed to apply configuration."); taskResult.TaskSucceeded = false; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs index 588cdab35..a90e148f7 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedComponent/Tasks/ElevatedInstallTask.cs @@ -100,7 +100,7 @@ public IAsyncOperation<ElevatedInstallTaskResult> InstallPackage(string packageI } catch (Exception e) { - _log.Error("Elevated app install failed.", e); + _log.Error(e, "Elevated app install failed."); result.TaskSucceeded = false; } diff --git a/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj b/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj index 2edbc2043..e123a8eba 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.ElevatedServer/DevHome.SetupFlow.ElevatedServer.csproj @@ -10,8 +10,8 @@ <ApplicationIcon Condition="'$(BuildRing)'=='Canary'">$(SolutionDir)\src\Assets\Canary\DevHome_Canary.ico</ApplicationIcon> <ApplicationIcon Condition="'$(BuildRing)'=='Stable'">$(SolutionDir)\src\Assets\Preview\DevHome_Preview.ico</ApplicationIcon> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> - <PublishProfileFullPath Condition="'$(BuildingInsideVisualStudio)' != 'True'">$(SolutionDir)\src\Properties\PublishProfiles\win10-$(Platform).pubxml</PublishProfileFullPath> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> + <PublishProfileFullPath Condition="'$(BuildingInsideVisualStudio)' != 'True'">$(SolutionDir)\src\Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfileFullPath> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <StartupObject>DevHome.SetupFlow.ElevatedServer.Program</StartupObject> @@ -22,7 +22,7 @@ <ProjectReference Include="..\DevHome.SetupFlow.ElevatedComponent\DevHome.SetupFlow.ElevatedComponent.csproj" /> </ItemGroup> - <!-- The server .exe requires the .winmd to be located next to it. The project reference + <!-- The server .exe requires the .winmd to be located next to it. The project reference is not enough to have it automatically placed there. Manually include it in the outputs. --> <ItemGroup> <Content Include="..\DevHome.SetupFlow.ElevatedComponent\bin\$(Platform)\$(Configuration)\$(TargetFramework)\DevHome.SetupFlow.ElevatedComponent.winmd"> diff --git a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj index 8fa0ddcfb..006b7ed1c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow.UnitTest/DevHome.SetupFlow.UnitTest.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.SetupFlow.UnitTest</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <IsPackable>false</IsPackable> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Assets/CreateVirtualEnvironment.png b/tools/SetupFlow/DevHome.SetupFlow/Assets/CreateVirtualEnvironment.png new file mode 100644 index 000000000..a8cb72c3a Binary files /dev/null and b/tools/SetupFlow/DevHome.SetupFlow/Assets/CreateVirtualEnvironment.png differ diff --git a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml index 43ba6925a..dc94e8e98 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml @@ -92,7 +92,7 @@ </StackPanel> <!-- Content --> - <ScrollViewer Grid.Row="3" Grid.ColumnSpan="2"> + <ScrollViewer Grid.Row="3" Grid.ColumnSpan="2" Visibility="{x:Bind ContentVisibility}"> <ContentControl IsTabStop="False" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" diff --git a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml.cs index 6a5cc42e1..a5f75b1c3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Controls/SetupShell.xaml.cs @@ -54,6 +54,12 @@ public Visibility HeaderVisibility get => (Visibility)GetValue(HeaderVisibilityProperty); set => SetValue(HeaderVisibilityProperty, value); } + + public Visibility ContentVisibility + { + get => (Visibility)GetValue(ContentVisibilityProperty); + set => SetValue(ContentVisibilityProperty, value); + } public SetupShell() { @@ -66,4 +72,5 @@ public SetupShell() public static readonly DependencyProperty HeaderProperty = DependencyProperty.RegisterAttached(nameof(Header), typeof(object), typeof(SetupShell), new PropertyMetadata(null)); public static readonly DependencyProperty OrchestratorProperty = DependencyProperty.RegisterAttached(nameof(Orchestrator), typeof(SetupFlowOrchestrator), typeof(SetupShell), new PropertyMetadata(null)); public static readonly DependencyProperty HeaderVisibilityProperty = DependencyProperty.RegisterAttached(nameof(HeaderVisibility), typeof(Visibility), typeof(SetupShell), new PropertyMetadata(Visibility.Visible)); + public static readonly DependencyProperty ContentVisibilityProperty = DependencyProperty.RegisterAttached(nameof(ContentVisibility), typeof(Visibility), typeof(SetupShell), new PropertyMetadata(Visibility.Visible)); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj index 29cc40326..2cb617316 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj +++ b/tools/SetupFlow/DevHome.SetupFlow/DevHome.SetupFlow.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.SetupFlow</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <UseWinUI>true</UseWinUI> </PropertyGroup> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Exceptions/AdaptiveCardNotRetrievedException.cs b/tools/SetupFlow/DevHome.SetupFlow/Exceptions/AdaptiveCardNotRetrievedException.cs new file mode 100644 index 000000000..d914e7bcc --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Exceptions/AdaptiveCardNotRetrievedException.cs @@ -0,0 +1,18 @@ +// 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; + +namespace DevHome.SetupFlow.Exceptions; + +public class AdaptiveCardNotRetrievedException : Exception +{ + public AdaptiveCardNotRetrievedException(string message) + : base(message) + { + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs b/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs index d59e104b7..d51f38047 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Extensions/ServiceExtensions.cs @@ -10,6 +10,7 @@ using DevHome.SetupFlow.Services.WinGet.Operations; using DevHome.SetupFlow.TaskGroups; using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -33,6 +34,7 @@ public static IServiceCollection AddSetupFlow(this IServiceCollection services, services.AddReview(); services.AddSummary(); services.AddSummaryInformation(); + services.AddCreateEnvironment(); // View-models services.AddSingleton<SetupFlowViewModel>(); @@ -194,4 +196,18 @@ private static IServiceCollection AddSetupTarget(this IServiceCollection service return services; } + + private static IServiceCollection AddCreateEnvironment(this IServiceCollection services) + { + // Task groups + services.AddTransient<EnvironmentCreationOptionsTaskGroup>(); + services.AddTransient<SelectEnvironmentProviderTaskGroup>(); + + // View models + services.AddTransient<CreateEnvironmentReviewViewModel>(); + services.AddTransient<EnvironmentCreationOptionsViewModel>(); + services.AddTransient<SelectEnvironmentProviderViewModel>(); + + return services; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs index 0a9e2f085..440dd217f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloneRepoTask.cs @@ -232,7 +232,7 @@ IAsyncOperation<TaskFinishedState> ISetupTask.Execute() if (result.Status == ProviderOperationStatus.Failure) { - _log.Error($"Could not clone {RepositoryToClone.DisplayName} because {result.DisplayMessage}", result.ExtendedError); + _log.Error(result.ExtendedError, $"Could not clone {RepositoryToClone.DisplayName} because {result.DisplayMessage}"); TelemetryFactory.Get<ITelemetry>().LogError("CloneTask_CouldNotClone_Event", LogLevel.Critical, new ExceptionEvent(result.ExtendedError.HResult, result.DisplayMessage)); _actionCenterErrorMessage.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoErrorForActionCenter, RepositoryToClone.DisplayName, result.DisplayMessage); @@ -242,7 +242,7 @@ IAsyncOperation<TaskFinishedState> ISetupTask.Execute() } catch (Exception e) { - _log.Error($"Could not clone {RepositoryToClone.DisplayName}", e); + _log.Error(e, $"Could not clone {RepositoryToClone.DisplayName}"); _actionCenterErrorMessage.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.CloneRepoErrorForActionCenter, RepositoryToClone.DisplayName, e.HResult.ToString("X", CultureInfo.CurrentCulture)); TelemetryFactory.Get<ITelemetry>().LogError("CloneTask_CouldNotClone_Event", LogLevel.Critical, new ExceptionEvent(e.HResult)); return TaskFinishedState.Failure; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs index eebbf5f94..a88c3c2d6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CloningInformation.cs @@ -124,7 +124,7 @@ public void SetIcon(ElementTheme theme) } catch (Exception e) { - _log.Error(e.Message, e); + _log.Error(e, e.Message); RepositoryTypeIcon = GetGitIcon(theme); return; } @@ -259,7 +259,7 @@ public string RepositoryProviderDisplayName } catch (Exception e) { - _log.Error(e.Message, e); + _log.Error(e, e.Message); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs index a7fe05d62..c97ca7298 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTargetTask.cs @@ -214,7 +214,7 @@ public void OnApplyConfigurationOperationChanged(object sender, SDK.Configuratio } catch (Exception ex) { - _log.Error($"Failed to process configuration progress data on target machine.'{ComputeSystemName}'", ex); + _log.Error(ex, $"Failed to process configuration progress data on target machine.'{ComputeSystemName}'"); } } @@ -254,7 +254,7 @@ public void HandleCompletedOperation(SDK.ApplyConfigurationResult applyConfigura if (resultStatus == ProviderOperationStatus.Failure) { - _log.Error($"Extension failed to configure config file with exception. Diagnostic text: {result.DiagnosticText}", result.ExtendedError); + _log.Error(result.ExtendedError, $"Extension failed to configure config file with exception. Diagnostic text: {result.DiagnosticText}"); throw new SDKApplyConfigurationSetResultException(applyConfigurationResult.Result.DiagnosticText); } @@ -288,7 +288,7 @@ public void HandleCompletedOperation(SDK.ApplyConfigurationResult applyConfigura } catch (Exception ex) { - _log.Error($"Failed to apply configuration on target machine. '{ComputeSystemName}'", ex); + _log.Error(ex, $"Failed to apply configuration on target machine. '{ComputeSystemName}'"); } var tempResultInfo = !string.IsNullOrEmpty(resultInformation) ? resultInformation : string.Empty; @@ -371,7 +371,7 @@ public IAsyncOperation<TaskFinishedState> Execute() } catch (Exception e) { - _log.Error($"Failed to apply configuration on target machine.", e); + _log.Error(e, $"Failed to apply configuration on target machine."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs index 2235ebeea..353e74ba9 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/ConfigureTask.cs @@ -107,7 +107,7 @@ IAsyncOperation<TaskFinishedState> ISetupTask.Execute() } catch (Exception e) { - _log.Error($"Failed to apply configuration.", e); + _log.Error(e, $"Failed to apply configuration."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs index 19e6869af..bd43532b0 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/CreateDevDriveTask.cs @@ -129,7 +129,7 @@ IAsyncOperation<TaskFinishedState> ISetupTask.ExecuteAsAdmin(IElevatedComponentO catch (Exception ex) { result = ex.HResult; - _log.Error($"Failed to create Dev Drive.", ex); + _log.Error(ex, $"Failed to create Dev Drive."); _actionCenterMessages.PrimaryMessage = _stringResource.GetLocalized(StringResourceKey.DevDriveErrorWithReason, _stringResource.GetLocalizedErrorMsg(ex.HResult, Identity.Component.DevDrive)); TelemetryFactory.Get<ITelemetry>().LogException("CreatingDevDriveException", ex); return TaskFinishedState.Failure; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs new file mode 100644 index 000000000..41d541f11 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +extern alias Projection; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.Common.Environments.Models; +using DevHome.Common.Environments.Services; +using DevHome.Common.Models; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; +using Projection::DevHome.SetupFlow.ElevatedComponent; +using Serilog; +using Windows.Foundation; +using DevHomeSDK = Microsoft.Windows.DevHome.SDK; + +namespace DevHome.SetupFlow.Models; + +/// <summary> +/// Task that creates an environment using the user input from an adaptive card session. +/// </summary> +public sealed class CreateEnvironmentTask : ISetupTask, IDisposable, IRecipient<CreationAdaptiveCardSessionEndedMessage> +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CreateEnvironmentTask)); + + private readonly IComputeSystemManager _computeSystemManager; + + private readonly TaskMessages _taskMessages; + + private readonly ActionCenterMessages _actionCenterMessages = new(); + + private readonly ISetupFlowStringResource _stringResource; + + // Used to signal the task to start the creation operation. This is used when the adaptive card session ends. + private readonly AutoResetEvent _autoResetEventToStartCreationOperation = new(false); + + private readonly SetupFlowViewModel _setupFlowViewModel; + + private bool _disposedValue; + + public event ISetupTask.ChangeMessageHandler AddMessage; + + public string UserJsonInput { get; set; } + + public ComputeSystemProviderDetails ProviderDetails { get; set; } + + public DeveloperIdWrapper DeveloperIdWrapper { get; set; } + + // The "#pragma warning disable 67" directive suppresses the CS0067 warning. + // CS0067 is a warning that occurs when a public event is declared but never used. +#pragma warning disable 67 + public event ISetupTask.ChangeActionCenterMessageHandler UpdateActionCenterMessage; +#pragma warning restore 67 + + public bool RequiresAdmin => false; + + public bool RequiresReboot => false; + + public bool DependsOnDevDriveToBeInstalled => false; + + public bool CreationOperationStarted { get; private set; } + + public ISummaryInformationViewModel SummaryScreenInformation { get; } + + public CreateEnvironmentTask(IComputeSystemManager computeSystemManager, ISetupFlowStringResource stringResource, SetupFlowViewModel setupFlowViewModel) + { + _computeSystemManager = computeSystemManager; + _stringResource = stringResource; + _taskMessages = new TaskMessages + { + Executing = _stringResource.GetLocalized(StringResourceKey.StartingEnvironmentCreation), + Finished = _stringResource.GetLocalized(StringResourceKey.EnvironmentCreationOperationInitializationFinished), + Error = _stringResource.GetLocalized(StringResourceKey.EnvironmentCreationError), + }; + _setupFlowViewModel = setupFlowViewModel; + _setupFlowViewModel.EndSetupFlow += OnEndSetupFlow; + + // Register for the adaptive card session ended message so we can use the session data to create the environment + WeakReferenceMessenger.Default.Register<CreationAdaptiveCardSessionEndedMessage>(this); + } + + public ActionCenterMessages GetErrorMessages() => _actionCenterMessages; + + public TaskMessages GetLoadingMessages() => _taskMessages; + + public ActionCenterMessages GetRebootMessage() => new(); + + /// <summary> + /// Receives the adaptive card session ended message from the he <see cref="ViewModels.Environments.EnvironmentCreationOptionsViewModel"/> + /// once the extension sends the session ended event. + /// </summary> + /// <param name="message"> + /// The message payload that contains the provider and the user input json that will be used to invoke the + /// <see cref="DevHomeSDK.IComputeSystemProvider.CreateCreateComputeSystemOperation(DevHomeSDK.IDeveloperId, string)"/> + /// </param> + public void Receive(CreationAdaptiveCardSessionEndedMessage message) + { + _log.Information("The extension sent the session ended event"); + ProviderDetails = message.Value.ProviderDetails; + + // Json input that the user entered in the adaptive card session + UserJsonInput = message.Value.UserInputResultJson; + + // In the future we'll add the specific developer ID to the task, but for now since we haven't + // add support for switching between developer Id's in the environments pages, we'll use the first one + // in the provider details list of developer IDs. If we get here, then there should be at least one. + DeveloperIdWrapper = message.Value.ProviderDetails.DeveloperIds.First(); + + _log.Information("Signaling to the waiting event handle to Continue the 'Execute' operation"); + _autoResetEventToStartCreationOperation.Set(); + } + + private void OnEndSetupFlow(object sender, EventArgs e) + { + WeakReferenceMessenger.Default.Unregister<CreationAdaptiveCardSessionEndedMessage>(this); + _setupFlowViewModel.EndSetupFlow -= OnEndSetupFlow; + } + + IAsyncOperation<TaskFinishedState> ISetupTask.Execute() + { + return Task.Run(() => + { + _log.Information("Executing the operation. Waiting to be signalled that the adaptive card session has ended"); + + // Either wait until we're signaled to continue execution or we times out after 1 minute. If this task is initiated + // then that means the user went past the review page. At this point the extension should be firing a session ended + // event. Since the call flow is disjointed an extension may not have sent the session ended event when this method is called. + _autoResetEventToStartCreationOperation.WaitOne(TimeSpan.FromMinutes(1)); + + if (string.IsNullOrWhiteSpace(UserJsonInput)) + { + // The extension's creation adaptive card may not need user input. In that case, the user input will be null or empty. + _log.Information("UserJsonInput is null or empty."); + } + + // If the provider details are null, then we can't proceed with the operation. This happens if the auto event times out. + if (ProviderDetails == null) + { + _log.Error("ProviderDetails is null so we cannot proceed with executing the task"); + AddMessage(_stringResource.GetLocalized(StringResourceKey.EnvironmentCreationFailedToGetProviderInformation), MessageSeverityKind.Error); + return TaskFinishedState.Failure; + } + + var sdkCreateEnvironmentOperation = ProviderDetails.ComputeSystemProvider.CreateCreateComputeSystemOperation(DeveloperIdWrapper.DeveloperId, UserJsonInput); + var createComputeSystemOperationWrapper = new CreateComputeSystemOperation(sdkCreateEnvironmentOperation, ProviderDetails, UserJsonInput); + + // Start the operation, which returns immediately and runs in the background. + createComputeSystemOperationWrapper.StartOperation(); + AddMessage(_stringResource.GetLocalized(StringResourceKey.EnvironmentCreationForProviderStarted), MessageSeverityKind.Info); + + _computeSystemManager.AddRunningOperationForCreation(createComputeSystemOperationWrapper); + CreationOperationStarted = true; + + _log.Information("Successfully started the creation operation"); + return TaskFinishedState.Success; + }).AsAsyncOperation(); + } + + IAsyncOperation<TaskFinishedState> ISetupTask.ExecuteAsAdmin(IElevatedComponentOperation elevatedComponentOperation) + { + return Task.Run(() => + { + // No admin rights required for this task. This shouldn't ever be invoked since the RequiresAdmin property is always false. + _log.Error("Admin execution is not required for the create environment task"); + return TaskFinishedState.Failure; + }).AsAsyncOperation(); + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _autoResetEventToStartCreationOperation.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedData.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedData.cs new file mode 100644 index 000000000..30214eb83 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedData.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.Common.Environments.Models; + +namespace DevHome.SetupFlow.Models.Environments; + +/// <summary> +/// Data payload for when the <see cref="Microsoft.Windows.DevHome.SDK.IExtensionAdaptiveCardSession2"> +/// session Ends. This data is used to send the user input from an adaptive card session back to an object +/// that subscribes to the <see cref="Microsoft.Windows.DevHome.SDK.IExtensionAdaptiveCardSession2.Stopped"> +/// event. +/// </summary> +public class CreationAdaptiveCardSessionEndedData +{ + /// <summary> + /// Gets the JSON string of the user input from the adaptive card session + /// </summary> + public string UserInputResultJson { get; private set; } + + /// <summary> + /// Gets the provider details for the compute system provider. <see cref="ComputeSystemProviderDetails"/> + /// </summary> + public ComputeSystemProviderDetails ProviderDetails { get; private set; } + + public CreationAdaptiveCardSessionEndedData(string userInputResultJson, ComputeSystemProviderDetails providerDetails) + { + UserInputResultJson = userInputResultJson; + ProviderDetails = providerDetails; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedMessage.cs new file mode 100644 index 000000000..b6b843991 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationAdaptiveCardSessionEndedMessage.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace DevHome.SetupFlow.Models.Environments; + +/// <summary> +/// Message for sending the data payload for the <see cref="Microsoft.Windows.DevHome.SDK.IExtensionAdaptiveCardSession2"> +/// object's session back to a subscriber when the session ends. +/// </summary> +public class CreationAdaptiveCardSessionEndedMessage : ValueChangedMessage<CreationAdaptiveCardSessionEndedData> +{ + public CreationAdaptiveCardSessionEndedMessage(CreationAdaptiveCardSessionEndedData value) + : base(value) + { + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsReviewPageDataRequestMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsReviewPageDataRequestMessage.cs new file mode 100644 index 000000000..52a595de0 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsReviewPageDataRequestMessage.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace DevHome.SetupFlow.Models.Environments; + +/// <summary> +/// Message for requesting a rendered adaptive card that was created from a <see cref="DevHome.Common.Models.ExtensionAdaptiveCard."/> +/// object in one view model to a view. +/// </summary> +/// <remarks> +/// This is used when a view that displays an adaptive card needs to request the rendered adaptive card from the view model. +/// The view in this case would not want to using Binding to bind to the adaptive card, but instead request it and then manually +/// add it to its UI. This prevents xaml binding crashes with "Element is already the child of another element" exceptions. +/// </remarks> +public sealed class CreationOptionsReviewPageDataRequestMessage : RequestMessage<RenderedAdaptiveCard> +{ +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsViewPageRequestMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsViewPageRequestMessage.cs new file mode 100644 index 000000000..ed4d2796c --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationOptionsViewPageRequestMessage.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace DevHome.SetupFlow.Models.Environments; + +/// <summary> +/// Message for requesting a rendered adaptive card that was created from a <see cref="DevHome.Common.Models.ExtensionAdaptiveCard."/> +/// object in one view model to a view. +/// </summary> +/// <remarks> +/// This is used when a view that displays an adaptive card needs to request the rendered adaptive card from the view model. +/// The view in this case would not want to using Binding to bind to the adaptive card, but instead request it and then manually +/// add it to its UI. This prevents xaml binding crashes with "Element is already the child of another element" exceptions. +/// </remarks> +public sealed class CreationOptionsViewPageRequestMessage : RequestMessage<RenderedAdaptiveCard> +{ +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationProviderChangedMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationProviderChangedMessage.cs new file mode 100644 index 000000000..8ff238af5 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreationProviderChangedMessage.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.Messaging.Messages; +using DevHome.Common.Environments.Models; + +namespace DevHome.SetupFlow.Models.Environments; + +/// <summary> +/// Message for sending the <see cref="ComputeSystemProviderDetails"/> from one view model to +/// another view model when the provider changes. +/// </summary> +public class CreationProviderChangedMessage : ValueChangedMessage<ComputeSystemProviderDetails> +{ + public CreationProviderChangedMessage(ComputeSystemProviderDetails value) + : base(value) + { + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/NewAdaptiveCardAvailableMessage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/NewAdaptiveCardAvailableMessage.cs new file mode 100644 index 000000000..2f5493b92 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/NewAdaptiveCardAvailableMessage.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace DevHome.SetupFlow.Models.Environments; + +/// <summary> +/// Message for sending a rendered adaptive card that was created from a <see cref="DevHome.Common.Models.ExtensionAdaptiveCard."/> +/// object in one view model to a view. +/// </summary> +/// <remarks> +/// Since multiple view models can listen for this message in the setup flow, this object is used to to send the rendered adaptive card +/// as well as the current view model that is being displayed in the setup flow. Listeners can use the current view model in use +/// by the <see cref="Services.SetupFlowOrchestrator"/> to determine if they should display the adaptive card or not. +/// </remarks> +public class NewAdaptiveCardAvailableMessage : ValueChangedMessage<RenderedAdaptiveCardData> +{ + public NewAdaptiveCardAvailableMessage(RenderedAdaptiveCardData value) + : base(value) + { + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/RenderedAdaptiveCardData.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/RenderedAdaptiveCardData.cs new file mode 100644 index 000000000..d3c52776b --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/Environments/RenderedAdaptiveCardData.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AdaptiveCards.Rendering.WinUI3; + +namespace DevHome.SetupFlow.Models.Environments; + +/// <summary> +/// Data object that contains the rendered adaptive card and the current view model being used in +/// the setup flow by the <see cref="Services.SetupFlowOrchestrator"/>. +/// </summary> +public class RenderedAdaptiveCardData +{ + public object CurrentSetupFlowViewModel { get; private set; } + + public RenderedAdaptiveCard RenderedAdaptiveCard { get; set; } + + public RenderedAdaptiveCardData(object currentSetupFlowViewModel, RenderedAdaptiveCard renderedAdaptiveCard) + { + CurrentSetupFlowViewModel = currentSetupFlowViewModel; + RenderedAdaptiveCard = renderedAdaptiveCard; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs index d37f37fe9..cfb9c611f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/GenericRepository.cs @@ -51,22 +51,22 @@ public IAsyncAction CloneRepositoryAsync(string cloneDestination, IDeveloperId d } catch (RecurseSubmodulesException recurseException) { - _log.Error("Could not clone all sub modules", recurseException); + _log.Error(recurseException, "Could not clone all sub modules"); throw; } catch (UserCancelledException userCancelledException) { - _log.Error("The user stoped the clone operation", userCancelledException); + _log.Error(userCancelledException, "The user stoped the clone operation"); throw; } catch (NameConflictException nameConflictException) { - _log.Error(string.Empty, nameConflictException); + _log.Error(nameConflictException, nameConflictException.ToString()); throw; } catch (Exception e) { - _log.Error("Could not clone the repository", e); + _log.Error(e, "Could not clone the repository"); throw; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs index 96052be67..718ee3802 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/InstallPackageTask.cs @@ -153,7 +153,7 @@ IAsyncOperation<TaskFinishedState> ISetupTask.Execute() catch (Exception e) { ReportAppInstallFailedEvent(); - _log.Error($"Exception thrown while installing package.", e); + _log.Error(e, $"Exception thrown while installing package."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); @@ -189,7 +189,7 @@ IAsyncOperation<TaskFinishedState> ISetupTask.ExecuteAsAdmin(IElevatedComponentO catch (Exception e) { ReportAppInstallFailedEvent(); - _log.Error($"Exception thrown while installing package.", e); + _log.Error(e, $"Exception thrown while installing package."); return TaskFinishedState.Failure; } }).AsAsyncOperation(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs index 11610776a..71a414dae 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProvider.cs @@ -60,7 +60,7 @@ public RepositoryProvider(IExtensionWrapper extensionWrapper) public string DisplayName => _repositoryProvider.DisplayName; - public string ExtensionDisplayName => _extensionWrapper.Name; + public string ExtensionDisplayName => _extensionWrapper.ExtensionDisplayName; /// <summary> /// Starts the extension if it isn't running. @@ -77,7 +77,7 @@ public void StartIfNotRunning() } catch (Exception ex) { - _log.Error($"Could not get repository provider from extension.", ex); + _log.Error(ex, $"Could not get repository provider from extension."); } } @@ -199,7 +199,7 @@ public async Task<ExtensionAdaptiveCardPanel> GetLoginUiAsync() } catch (Exception ex) { - _log.Error($"ShowLoginUIAsync(): loginUIContentDialog failed.", ex); + _log.Error(ex, $"ShowLoginUIAsync(): loginUIContentDialog failed."); } return null; @@ -224,7 +224,7 @@ public IEnumerable<IDeveloperId> GetAllLoggedInAccounts() var developerIdsResult = _devIdProvider.GetLoggedInDeveloperIds(); if (developerIdsResult.Result.Status != ProviderOperationStatus.Success) { - _log.Error($"Could not get logged in accounts. Message: {developerIdsResult.Result.DisplayMessage}", developerIdsResult.Result.ExtendedError); + _log.Error(developerIdsResult.Result.ExtendedError, $"Could not get logged in accounts. Message: {developerIdsResult.Result.DisplayMessage}"); return new List<IDeveloperId>(); } @@ -251,7 +251,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } else @@ -264,7 +264,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } } @@ -282,7 +282,7 @@ public RepositorySearchInformation SearchForRepositories(IDeveloperId developerI } catch (Exception ex) { - _log.Error($"Could not get repositories. Message: {ex}"); + _log.Error(ex, $"Could not get repositories. Message: {ex}"); } _repositories[developerId] = repoSearchInformation.Repositories; @@ -304,7 +304,7 @@ public RepositorySearchInformation GetAllRepositories(IDeveloperId developerId) } else { - _log.Error($"Could not get repositories. Message: {result.Result.DisplayMessage}", result.Result.ExtendedError); + _log.Error(result.Result.ExtendedError, $"Could not get repositories. Message: {result.Result.DisplayMessage}"); } } catch (AggregateException aggregateException) @@ -316,12 +316,12 @@ public RepositorySearchInformation GetAllRepositories(IDeveloperId developerId) } else { - _log.Error(aggregateException.Message, aggregateException); + _log.Error(aggregateException, aggregateException.Message); } } catch (Exception ex) { - _log.Error($"Could not get repositories. Message: {ex}", ex); + _log.Error(ex, $"Could not get repositories. Message: {ex}"); } _repositories[developerId] = repoSearchInformation.Repositories; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs index 7fcdb8542..346ddb20e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/RepositoryProviders.cs @@ -37,7 +37,7 @@ public string DisplayName(string providerName) public RepositoryProviders(IEnumerable<IExtensionWrapper> extensionWrappers) { - _providers = extensionWrappers.ToDictionary(extensionWrapper => extensionWrapper.Name, extensionWrapper => new RepositoryProvider(extensionWrapper)); + _providers = extensionWrappers.ToDictionary(extensionWrapper => extensionWrapper.ExtensionDisplayName, extensionWrapper => new RepositoryProvider(extensionWrapper)); } public void StartAllExtensions() diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs index 9da80343a..f74b337de 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WinGetPackage.cs @@ -133,7 +133,7 @@ private string FindVersion(IReadOnlyList<PackageVersionId> availableVersions, Pa } catch (Exception e) { - _log.Error($"Unable to validate if the version {versionInfo.Version} is in the list of available versions", e); + _log.Error(e, $"Unable to validate if the version {versionInfo.Version} is in the list of available versions"); } return versionInfo.Version; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs b/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs index 3b2de8ab8..cabbf8755 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Models/WingetConfigure/SDKOpenConfigurationSetResult.cs @@ -49,7 +49,7 @@ public SDKOpenConfigurationSetResult(SDK.OpenConfigurationSetResult result, ISet public string GetErrorMessage() { var log = Log.ForContext("SourceContext", nameof(SDKOpenConfigurationSetResult)); - log.Error($"Extension failed to open the configuration file provided by Dev Home: Field: {Field}, Value: {Value}, Line: {Line}, Column: {Column}", ResultCode); + log.Error(ResultCode, $"Extension failed to open the configuration file provided by Dev Home: Field: {Field}, Value: {Value}, Line: {Line}, Column: {Column}"); return _setupFlowStringResource.GetLocalized(StringResourceKey.SetupTargetConfigurationOpenConfigFailed); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Selectors/ReviewTabViewSelector.cs b/tools/SetupFlow/DevHome.SetupFlow/Selectors/ReviewTabViewSelector.cs index 38d8d8655..db467aeaf 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Selectors/ReviewTabViewSelector.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Selectors/ReviewTabViewSelector.cs @@ -3,6 +3,7 @@ using System; using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -35,6 +36,11 @@ public DataTemplate SetupTargetTabTemplate get; set; } + public DataTemplate CreateEnvironmentTabTemplate + { + get; set; + } + protected override DataTemplate SelectTemplateCore(object item) { return ResolveDataTemplate(item, () => base.SelectTemplateCore(item)); @@ -59,6 +65,7 @@ private DataTemplate ResolveDataTemplate(object item, Func<DataTemplate> default RepoConfigReviewViewModel => RepoConfigTabTemplate, AppManagementReviewViewModel => AppManagementTabTemplate, SetupTargetReviewViewModel => SetupTargetTabTemplate, + CreateEnvironmentReviewViewModel => CreateEnvironmentTabTemplate, _ => defaultDataTemplate(), }; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs b/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs index 399b5a2f6..9aadd5a35 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Selectors/SetupFlowViewSelector.cs @@ -3,6 +3,7 @@ using System; using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -29,7 +30,11 @@ public class SetupFlowViewSelector : DataTemplateSelector public DataTemplate SummaryTemplate { get; set; } - public DataTemplate ConfigurationFileTemplate { get; set; } + public DataTemplate ConfigurationFileTemplate { get; set; } + + public DataTemplate SelectEnvironmentsProviderTemplate { get; set; } + + public DataTemplate EnvironmentCreationOptionsTemplate { get; set; } protected override DataTemplate SelectTemplateCore(object item) { @@ -58,7 +63,9 @@ private DataTemplate ResolveDataTemplate(object item, Func<DataTemplate> default LoadingViewModel => LoadingTemplate, SummaryViewModel => SummaryTemplate, ConfigurationFileViewModel => ConfigurationFileTemplate, - SetupTargetViewModel => SetupTargetTemplate, + SetupTargetViewModel => SetupTargetTemplate, + SelectEnvironmentProviderViewModel => SelectEnvironmentsProviderTemplate, + EnvironmentCreationOptionsViewModel => EnvironmentCreationOptionsTemplate, _ => defaultDataTemplate(), }; } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs index ca3c3f70b..690bfb7f3 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/AppManagementInitializer.cs @@ -74,7 +74,7 @@ private async Task InitializeWindowsPackageManagerAsync() } catch (Exception e) { - _log.Error($"Unable to correctly initialize app management at the moment. Further attempts will be performed later.", e); + _log.Error(e, $"Unable to correctly initialize app management at the moment. Further attempts will be performed later."); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs index 6ab0f24ab..26664c6fc 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/CatalogDataSourceLoader.cs @@ -72,7 +72,7 @@ private async Task InitializeDataSourceAsync(WinGetPackageDataSource dataSource) } catch (Exception e) { - _log.Error($"Exception thrown while initializing data source of type {dataSource.GetType().Name}", e); + _log.Error(e, $"Exception thrown while initializing data source of type {dataSource.GetType().Name}"); } } @@ -89,7 +89,7 @@ private async Task<IList<PackageCatalog>> LoadCatalogsFromDataSourceAsync(WinGet } catch (Exception e) { - _log.Error($"Exception thrown while loading data source of type {dataSource.GetType().Name}", e); + _log.Error(e, $"Exception thrown while loading data source of type {dataSource.GetType().Name}"); } return null; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs index 9efdd3a9e..ab4936a11 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ComputeSystemViewModelFactory.cs @@ -37,7 +37,7 @@ public async Task<ComputeSystemCardViewModel> CreateCardViewModelAsync( catch (Exception ex) { var log = Log.ForContext("SourceContext", nameof(ComputeSystemViewModelFactory)); - log.Error($"Failed to get initial properties for compute system {computeSystem}. Error: {ex.Message}"); + log.Error(ex, $"Failed to get initial properties for compute system {computeSystem}. Error: {ex.Message}"); } return cardViewModel; diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs index 21c498c6d..405950958 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/ConfigurationFileBuilder.cs @@ -138,7 +138,7 @@ private List<WinGetConfigResource> GetResourcesForCloneTaskGroup(RepoConfigTaskG } catch (Exception e) { - _log.Error($"Error creating a repository resource entry", e); + _log.Error(e, $"Error creating a repository resource entry"); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs index 1663c249b..d4b0dbeed 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/DevDriveManager.cs @@ -251,7 +251,7 @@ volumeLetter is char newLetter && volumeSize is ulong newSize && catch (Exception ex) { // Log then return empty list, don't show the user their existing dev drive. Not catastrophic failure. - _log.Error($"Failed to get existing Dev Drives.", ex); + _log.Error(ex, $"Failed to get existing Dev Drives."); return new List<IDevDrive>(); } } @@ -280,7 +280,7 @@ private DevDrive GetDevDriveWithDefaultInfo() } catch (Exception ex) { - _log.Error($"Unable to get available Free Space for {root}.", ex); + _log.Error(ex, $"Unable to get available Free Space for {root}."); validationSuccessful = false; } @@ -372,7 +372,7 @@ public ISet<DevDriveValidationResult> GetDevDriveValidationResults(IDevDrive dev } catch (Exception ex) { - _log.Error($"Failed to validate selected Drive letter ({devDrive.DriveLocation.FirstOrDefault()}).", ex); + _log.Error(ex, $"Failed to validate selected Drive letter ({devDrive.DriveLocation.FirstOrDefault()})."); returnSet.Add(DevDriveValidationResult.DriveLetterNotAvailable); } @@ -405,7 +405,7 @@ public IList<char> GetAvailableDriveLetters(char? usedLetterToKeepInList = null) } catch (Exception ex) { - _log.Error($"Failed to get Available Drive letters.", ex); + _log.Error(ex, $"Failed to get Available Drive letters."); } return driveLetterSet.ToList(); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs index 91c5820b2..93469e207 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/SetupFlowOrchestrator.cs @@ -7,10 +7,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AdaptiveCards.ObjectModel.WinUI3; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.DevHomeAdaptiveCards.Parsers; +using DevHome.Common.Renderers; using DevHome.SetupFlow.Common.Contracts; using DevHome.SetupFlow.Common.Elevation; +using DevHome.SetupFlow.Common.Helpers; using DevHome.SetupFlow.Models; using DevHome.SetupFlow.ViewModels; using Projection::DevHome.SetupFlow.ElevatedComponent; @@ -22,6 +27,7 @@ public enum SetupFlowKind { LocalMachine, SetupTarget, + CreateEnvironment, } /// <summary> @@ -31,6 +37,10 @@ public partial class SetupFlowOrchestrator : ObservableObject { private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SetupFlowOrchestrator)); + private readonly string _adaptiveCardNextButtonId = "DevHomeMachineConfigurationNextButton"; + + private readonly string _adaptiveCardPreviousButtonId = "DevHomeMachineConfigurationPreviousButton"; + private readonly List<SetupPageViewModelBase> _flowPages = new(); /// <summary> @@ -120,6 +130,13 @@ public IReadOnlyList<SetupPageViewModelBase> FlowPages public bool IsMachineConfigurationInProgress => FlowPages.Count > 1; + /// <summary> + /// Gets the renderer for the Dev Home action set. This is used to invoke the the buttons within the top level + /// of the adaptive card. This stitches up the setup flow's next and previous buttons to two buttons within an + /// extensions adaptive card. + /// </summary> + public DevHomeActionSet DevHomeActionSetRenderer { get; private set; } = new(TopLevelCardActionSetVisibility.Hidden); + /// <summary> /// Gets or sets a value indicating whether the done button should be shown. When false, the cancel /// hyperlink button will be shown in the UI. @@ -179,6 +196,13 @@ public void ReleaseRemoteOperationObject() [RelayCommand(CanExecute = nameof(CanGoToPreviousPage))] public async Task GoToPreviousPage() { + // If an adaptive card is being shown in the setup flow, we need to invoke the action + // of the previous button in the action set to move the flow to the previous page in the adaptive card. + if (DevHomeActionSetRenderer?.ActionButtonInvoker != null) + { + DevHomeActionSetRenderer.InitiateAction(_adaptiveCardPreviousButtonId); + } + await SetCurrentPageIndex(_currentPageIndex - 1); } @@ -190,6 +214,17 @@ private bool CanGoToPreviousPage() [RelayCommand(CanExecute = nameof(CanGoToNextPage))] public async Task GoToNextPage() { + // If an adaptive card is being shown in the setup flow, we need to invoke the action + // of the primary button in the action set to move the flow to the next page in the adaptive card. + if (DevHomeActionSetRenderer?.ActionButtonInvoker != null) + { + if (!TryNavigateToNextAdaptiveCardPage(_adaptiveCardNextButtonId)) + { + // Don't navigate if there were validation errors. + return; + } + } + await SetCurrentPageIndex(_currentPageIndex + 1); } @@ -243,4 +278,25 @@ private async Task SetCurrentPageIndex(int index) await CurrentPageViewModel?.OnNavigateToAsync(); } + + /// <summary> + /// Performs the work needed to navigate to the next page in an adaptive card. This is used when the setup flow is + /// rendering a flow that includes an adaptive card style wizard flow. + /// </summary> + /// <remarks> + /// Only adaptive cards that have input controls with the 'isRequired' property set to true will be validated. + /// All other elements within the adaptive card will be ignored. + /// </remarks> + /// <param name="buttonId">The string Id of the button</param> + /// <returns>True when the user inputs have been validated and false otherwise.</returns> + private bool TryNavigateToNextAdaptiveCardPage(string buttonId) + { + if (DevHomeActionSetRenderer.TryValidateAndInitiateAction(buttonId, CurrentPageViewModel?.GetAdaptiveCardUserInputsForNavigationValidation())) + { + return true; + } + + _log.Warning($"Failed to invoke adaptive card action with Id: {buttonId} due to input validation failure"); + return false; + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs index a1d3cb4f0..9535a5eaa 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/StringResourceKey.cs @@ -261,4 +261,28 @@ public static class StringResourceKey public static readonly string SetupTargetConfigurationProgressUpdate = nameof(SetupTargetConfigurationProgressUpdate); public static readonly string SetupTargetConfigurationUnitProgressErrorWithMsg = nameof(SetupTargetConfigurationUnitProgressErrorWithMsg); public static readonly string SetupTargetConfigurationUnitUnknown = nameof(SetupTargetConfigurationUnitUnknown); + + // Create Environment flow + public static readonly string SelectEnvironmentPageTitle = nameof(SelectEnvironmentPageTitle); + public static readonly string ConfigureEnvironmentPageTitle = nameof(ConfigureEnvironmentPageTitle); + public static readonly string EnvironmentCreationReviewPageTitle = nameof(EnvironmentCreationReviewPageTitle); + public static readonly string EnvironmentCreationReviewTabTitle = nameof(EnvironmentCreationReviewTabTitle); + public static readonly string EnvironmentCreationError = nameof(EnvironmentCreationError); + public static readonly string StartingEnvironmentCreation = nameof(StartingEnvironmentCreation); + public static readonly string EnvironmentCreationOperationInitializationFinished = nameof(EnvironmentCreationOperationInitializationFinished); + public static readonly string EnvironmentCreationForProviderStarted = nameof(EnvironmentCreationForProviderStarted); + public static readonly string EnvironmentCreationFailedToGetProviderInformation = nameof(EnvironmentCreationFailedToGetProviderInformation); + public static readonly string EnvironmentCreationReviewExpanderDescription = nameof(EnvironmentCreationReviewExpanderDescription); + public static readonly string CreateEnvironmentButtonText = nameof(CreateEnvironmentButtonText); + public static readonly string SetupShellReviewPageDescriptionForEnvironmentCreation = nameof(SetupShellReviewPageDescriptionForEnvironmentCreation); + + // Summary page + public static readonly string SummaryPageOpenDashboard = nameof(SummaryPageOpenDashboard); + public static readonly string SummaryPageRedirectToEnvironmentPageButton = nameof(SummaryPageRedirectToEnvironmentPageButton); + public static readonly string SummaryPageHeader = nameof(SummaryPageHeader); + public static readonly string SummaryPageHeaderForEnvironmentCreationText = nameof(SummaryPageHeaderForEnvironmentCreationText); + + // Review page + public static readonly string ReviewExpanderDescription = nameof(ReviewExpanderDescription); + public static readonly string SetupShellReviewPageDescription = nameof(SetupShellReviewPageDescription); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs index f19524036..d70f08539 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetCatalogConnector.cs @@ -223,7 +223,7 @@ private async Task CreateAndConnectSearchCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to search catalog.", e); + _log.Error(e, $"Failed to create or connect to search catalog."); } } @@ -241,7 +241,7 @@ private async Task CreateAndConnectWinGetCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to 'winget' catalog source.", e); + _log.Error(e, $"Failed to create or connect to 'winget' catalog source."); } } @@ -259,7 +259,7 @@ private async Task CreateAndConnectMsStoreCatalogAsync() } catch (Exception e) { - _log.Error($"Failed to create or connect to 'msstore' catalog source.", e); + _log.Error(e, $"Failed to create or connect to 'msstore' catalog source."); } } @@ -278,7 +278,7 @@ private async Task<WinGetCatalog> CreateAndConnectCustomCatalogAsync(string cata } catch (Exception e) { - _log.Error($"Failed to create or connect to custom catalog with name {catalogName}", e); + _log.Error(e, $"Failed to create or connect to custom catalog with name {catalogName}"); return null; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs index ca212e18b..8511c9fa8 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetDeployment.cs @@ -53,7 +53,7 @@ await Task.Run(() => } catch (Exception e) { - _log.Error($"Failed to create dummy {nameof(PackageManager)} COM object. WinGet COM Server is not available.", e); + _log.Error(e, $"Failed to create dummy {nameof(PackageManager)} COM object. WinGet COM Server is not available."); return false; } } @@ -70,7 +70,7 @@ public async Task<bool> IsUpdateAvailableAsync() } catch (Exception e) { - _log.Error("Failed to check if AppInstaller has an update, defaulting to false", e); + _log.Error(e, "Failed to check if AppInstaller has an update, defaulting to false"); return false; } } @@ -87,12 +87,12 @@ public async Task<bool> RegisterAppInstallerAsync() } catch (RegisterPackageException e) { - _log.Error($"Failed to register AppInstaller", e); + _log.Error(e, $"Failed to register AppInstaller"); return false; } catch (Exception e) { - _log.Error("An unexpected error occurred when registering AppInstaller", e); + _log.Error(e, "An unexpected error occurred when registering AppInstaller"); return false; } } @@ -106,7 +106,7 @@ public async Task<bool> IsConfigurationUnstubbedAsync() } catch (Exception e) { - _log.Error("An unexpected error occurred when checking if configuration is unstubbed", e); + _log.Error(e, "An unexpected error occurred when checking if configuration is unstubbed"); return false; } } @@ -123,7 +123,7 @@ public async Task<bool> UnstubConfigurationAsync() } catch (Exception e) { - _log.Error("An unexpected error occurred when unstubbing configuration", e); + _log.Error(e, "An unexpected error occurred when unstubbing configuration"); return false; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs index c4a1006d6..1212b1d7e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGet/WinGetRecovery.cs @@ -45,12 +45,12 @@ public async Task<T> DoWithRecoveryAsync<T>(Func<Task<T>> actionFunc) } catch (CatalogNotInitializedException e) { - _log.Error($"Catalog used by the action is not initialized", e); + _log.Error(e, $"Catalog used by the action is not initialized"); await RecoveryAsync(attempt); } catch (COMException e) when (e.HResult == RpcServerUnavailable || e.HResult == RpcCallFailed || e.HResult == PackageUpdating) { - _log.Error($"Failed to operate on out-of-proc object with error code: 0x{e.HResult:x}", e); + _log.Error(e, $"Failed to operate on out-of-proc object with error code: 0x{e.HResult:x}"); await RecoveryAsync(attempt); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs index b58908efb..9209e267a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetFeaturedApplicationsDataSource.cs @@ -90,7 +90,7 @@ await ForEachEnabledExtensionAsync(async (extensionGroups) => } catch (Exception e) { - _log.Error($"Error loading packages from featured applications group.", e); + _log.Error(e, $"Error loading packages from featured applications group."); } } }); @@ -171,7 +171,7 @@ private async Task ForEachEnabledExtensionAsync(Func<IReadOnlyList<IFeaturedAppl var extensions = await _extensionService.GetInstalledExtensionsAsync(ProviderType.FeaturedApplications); foreach (var extension in extensions) { - var extensionName = extension.Name; + var extensionName = extension.PackageFamilyName; try { _log.Information($"Getting featured applications provider from extension '{extensionName}'"); @@ -197,7 +197,7 @@ private async Task ForEachEnabledExtensionAsync(Func<IReadOnlyList<IFeaturedAppl } else { - _log.Error($"Failed to get featured applications groups from extension '{extensionName}': {groupsResult.Result.DiagnosticText}", groupsResult.Result.ExtendedError); + _log.Error(groupsResult.Result.ExtendedError, $"Failed to get featured applications groups from extension '{extensionName}': {groupsResult.Result.DiagnosticText}"); } } else @@ -207,7 +207,7 @@ private async Task ForEachEnabledExtensionAsync(Func<IReadOnlyList<IFeaturedAppl } catch (Exception e) { - _log.Error($"Error loading featured applications from extension {extensionName}", e); + _log.Error(e, $"Error loading featured applications from extension {extensionName}"); } } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs index e58341c59..3b1bdadb5 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageJsonDataSource.cs @@ -159,7 +159,7 @@ private async Task<PackageCatalog> LoadCatalogAsync(JsonWinGetPackageCatalog jso } catch (Exception e) { - _log.Error($"Error loading packages from winget catalog.", e); + _log.Error(e, $"Error loading packages from winget catalog."); } return null; @@ -184,7 +184,7 @@ private async Task<IRandomAccessStream> GetJsonApplicationIconAsync(JsonWinGetPa } catch (Exception e) { - _log.Error($"Failed to get icon for JSON package {package.Uri}.", e); + _log.Error(e, $"Failed to get icon for JSON package {package.Uri}."); } _log.Warning($"No icon found for JSON package {package.Uri}. A default one will be provided."); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs index d87fe3126..fa9bb0b06 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Services/WinGetPackageRestoreDataSource.cs @@ -97,7 +97,7 @@ public async override Task<IList<PackageCatalog>> LoadCatalogsAsync() } catch (Exception e) { - _log.Error($"Error loading packages from winget restore catalog.", e); + _log.Error(e, $"Error loading packages from winget restore catalog."); } return result; @@ -130,7 +130,7 @@ private async Task<IRandomAccessStream> GetRestoreApplicationIconAsync(IRestoreA } catch (Exception e) { - _log.Error($"Failed to get icon for restore package {appInfo.Id}", e); + _log.Error(e, $"Failed to get icon for restore package {appInfo.Id}"); } _log.Warning($"No {theme} icon found for restore package {appInfo.Id}. A default one will be provided."); diff --git a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw index 2f00a48b7..6bee36d1b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw +++ b/tools/SetupFlow/DevHome.SetupFlow/Strings/en-us/Resources.resw @@ -649,7 +649,7 @@ <value>Generate a WinGet Configuration file (.winget) to repeat this set up in the future or share it with others.</value> <comment>{Locked="WinGet",".winget"}Tooltip text about the generated configuration file</comment> </data> - <data name="Review_SetupDetails.Text" xml:space="preserve"> + <data name="ReviewExpanderDescription" xml:space="preserve"> <value>Set up details</value> <comment>Header for a section detailing the set up steps to be performed. "Set up" is the noun</comment> </data> @@ -713,10 +713,14 @@ <value>Select a target machine to set up.</value> <comment>Description for the setup target page</comment> </data> - <data name="SetupShell_Review.Description" xml:space="preserve"> + <data name="SetupShellReviewPageDescription" xml:space="preserve"> <value>Review the terms and setup details below before applying these changes to your computer.</value> <comment>Description for the review page</comment> </data> + <data name="SetupShellReviewPageDescriptionForEnvironmentCreation" xml:space="preserve"> + <value>Review the details for your new environment</value> + <comment>Description for the review page</comment> + </data> <data name="Applications" xml:space="preserve"> <value>Applications</value> <comment>Header for a section showing a summary of applications to be installed</comment> @@ -1044,7 +1048,7 @@ <value>Installed applications</value> <comment>Header for the section that shows all the downloaded apps</comment> </data> - <data name="SummaryPage_Header.Text" xml:space="preserve"> + <data name="SummaryPageHeader" xml:space="preserve"> <value>Here's what we set up for you</value> <comment>Header text for the summary page</comment> </data> @@ -1056,7 +1060,7 @@ <value>Next steps</value> <comment>Text for the "Next steps" section of the summary page</comment> </data> - <data name="SummaryPage_OpenDashboard.Content" xml:space="preserve"> + <data name="SummaryPageOpenDashboard" xml:space="preserve"> <value>Open Dashboard</value> <comment>Button content to let user go to the dashboard</comment> </data> @@ -1732,4 +1736,91 @@ <value>View file</value> <comment>Button content for viewing a file.</comment> </data> + <data name="SelectEnvironmentPageTitle" xml:space="preserve"> + <value>Select your environment</value> + <comment>Title of the 'Select your environment' page in the create environment flow where users can select the provider they will use to create the environment, like Microsoft Hyper-V and Microsoft Dev Box</comment> + </data> + <data name="ConfigureEnvironmentPageTitle" xml:space="preserve"> + <value>Configure your environment</value> + <comment>Title of the 'Configure your environment' page in the environment creation flow where users can choose options that they'd like their environment to be created with</comment> + </data> + <data name="SelectEnvironmentSubtitle.Text" xml:space="preserve"> + <value>Begin by selecting an environment provider below</value> + <comment>subtitle text advise the user that they can select an environment option in a list below the text</comment> + </data> + <data name="MainPageCreateEnvironment.Header" xml:space="preserve"> + <value>Create virtual environment</value> + <comment>Header for a card that when clicked takes the user to a multi-step flow for creating an environment</comment> + </data> + <data name="MainPageCreateEnvironment.Description" xml:space="preserve"> + <value>Create a local or cloud environment</value> + <comment>Body text description for a card than when clicked takes the user to a multi-step flow for creating an environment</comment> + </data> + <data name="SelectEnvironmentPage.Description" xml:space="preserve"> + <value>Choose an environment provider to create a new dev environment</value> + <comment>Description for the setup target page</comment> + </data> + <data name="ConfigureEnvironmentPage.Description" xml:space="preserve"> + <value>Add options to create your environment</value> + <comment>Description for the setup target page</comment> + </data> + <data name="ErrorRetrievingAdaptiveCardSession.Title" xml:space="preserve"> + <value>There was an error retreiving the adaptive card session from the extension</value> + <comment>Error text display when we are unable to retrieve the adaptive card information from an extension</comment> + </data> + <data name="EnvironmentCreationReviewTabTitle" xml:space="preserve"> + <value>Environment</value> + <comment>Title for create environment review tab</comment> + </data> + <data name="EnvironmentCreationReviewExpanderDescription" xml:space="preserve"> + <value>Your environment's details</value> + <comment>Title for create environment review tab</comment> + </data> + <data name="EnvironmentCreationReviewPageTitle" xml:space="preserve"> + <value>Review your environment</value> + <comment>Title for create environment review page</comment> + </data> + <data name="CreateEnvironmentButtonText" xml:space="preserve"> + <value>Create Environment</value> + </data> + <data name="EnvironmentCreationError" xml:space="preserve"> + <value>There was an error starting the creation operation</value> + <comment>Text to tell the user that we couldn't start the operation to create their local or cloud virtual environment</comment> + </data> + <data name="EnvironmentCreationOperationInitializationFinished" xml:space="preserve"> + <value>The operation to create your environment has started successfully</value> + <comment>Text to tell the user that we were able to start the operation that create their local or cloud virtual machine successfully</comment> + </data> + <data name="StartingEnvironmentCreation" xml:space="preserve"> + <value>Starting the create environment operation</value> + <comment>Text to tell the user that the operation to create their local or cloud virtual environment has started</comment> + </data> + <data name="EnvironmentCreationFailedToGetProviderInformation" xml:space="preserve"> + <value>We timed out waiting for the extension to provide us with information to create your environment</value> + <comment>Error text to show the user that we timed out while waiting for a response from a Dev Home extension</comment> + </data> + <data name="EnvironmentCreationForProviderStarted" xml:space="preserve"> + <value>The {0} provider is now creating your environment</value> + <comment>{Locked="{0}"} Text that tells the user that a specific provider has started creating their environment. {0} is the name of a provider who Dev Home will send the request to.</comment> + </data> + <data name="EnvironmentCreationOperationFailedToStart" xml:space="preserve"> + <value>We failed to start the creation operation for the {0} provider</value> + <comment>{Locked="{0}"} Text that tells the user that Dev Home could not start the creation operation for a specific provider. {0} is the name of a provider who Dev Home will send the request to.</comment> + </data> + <data name="SummaryPageHeadingForCreateEnvironmentFlow.Text" xml:space="preserve"> + <value>Environment details</value> + <comment>Heading that will show the user the details of the environment they initiated the creation process for. Environments can be local or cloud virtual machines</comment> + </data> + <data name="SummaryPageRedirectToEnvironmentPageButton" xml:space="preserve"> + <value>Go to Environments page</value> + <comment>Text for button that when clicked will redirect the user to the environments page in Dev Home</comment> + </data> + <data name="SummaryPageEnvironmentCreating.Text" xml:space="preserve"> + <value>Environment being created</value> + <comment>Text to tell the user that an environment is being created. Environments can be local or cloud virtual machines</comment> + </data> + <data name="SummaryPageHeaderForEnvironmentCreationText" xml:space="preserve"> + <value>We've started creating your environment</value> + <comment>Text to tell the user that an environment is being created. Environments can be local or cloud virtual machines</comment> + </data> </root> \ No newline at end of file diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs new file mode 100644 index 000000000..130377a53 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/EnvironmentCreationOptionsTaskGroup.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using DevHome.Common.Environments.Models; +using DevHome.Common.Environments.Services; +using DevHome.SetupFlow.Models; +using DevHome.SetupFlow.Services; +using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; + +namespace DevHome.SetupFlow.TaskGroups; + +public class EnvironmentCreationOptionsTaskGroup : ISetupTaskGroup +{ + private readonly ISetupFlowStringResource _setupFlowStringResource; + + private readonly IComputeSystemManager _computeSystemManager; + + private readonly EnvironmentCreationOptionsViewModel _environmentCreationOptionsViewModel; + + private readonly CreateEnvironmentReviewViewModel _createEnvironmentReviewViewModel; + + public ComputeSystemProviderDetails ProviderDetails { get; private set; } + + public CreateEnvironmentTask CreateEnvironmentTask { get; private set; } + + public EnvironmentCreationOptionsTaskGroup( + SetupFlowViewModel setupFlowViewModel, + IComputeSystemManager computeSystemManager, + ISetupFlowStringResource setupFlowStringResource, + EnvironmentCreationOptionsViewModel environmentCreationOptionsViewModel, + CreateEnvironmentReviewViewModel createEnvironmentReviewViewModel) + { + _environmentCreationOptionsViewModel = environmentCreationOptionsViewModel; + _createEnvironmentReviewViewModel = createEnvironmentReviewViewModel; + _setupFlowStringResource = setupFlowStringResource; + _computeSystemManager = computeSystemManager; + CreateEnvironmentTask = new CreateEnvironmentTask(_computeSystemManager, _setupFlowStringResource, setupFlowViewModel); + } + + public IEnumerable<ISetupTask> SetupTasks => new List<ISetupTask>() { CreateEnvironmentTask }; + + public IEnumerable<ISetupTask> DSCTasks => new List<ISetupTask>(); + + public SetupPageViewModelBase GetSetupPageViewModel() => _environmentCreationOptionsViewModel; + + public ReviewTabViewModelBase GetReviewTabViewModel() => _createEnvironmentReviewViewModel; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SelectEnvironmentProviderTaskGroup.cs b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SelectEnvironmentProviderTaskGroup.cs new file mode 100644 index 000000000..fcb18c29e --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/TaskGroups/SelectEnvironmentProviderTaskGroup.cs @@ -0,0 +1,34 @@ +// 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 DevHome.SetupFlow.Models; +using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; + +namespace DevHome.SetupFlow.TaskGroups; + +public class SelectEnvironmentProviderTaskGroup : ISetupTaskGroup +{ + private readonly SelectEnvironmentProviderViewModel _selectEnvironmentProviderViewModel; + + public SelectEnvironmentProviderTaskGroup(SelectEnvironmentProviderViewModel selectEnvironmentProviderViewModel) + { + _selectEnvironmentProviderViewModel = selectEnvironmentProviderViewModel; + } + + // No setup tasks needed for this task group. + public IEnumerable<ISetupTask> SetupTasks => new List<ISetupTask>(); + + // No dsc tasks needed for this task group. + public IEnumerable<ISetupTask> DSCTasks => new List<ISetupTask>(); + + public SetupPageViewModelBase GetSetupPageViewModel() => _selectEnvironmentProviderViewModel; + + // Review tab not needed for this task group. + public ReviewTabViewModelBase GetReviewTabViewModel() => null; +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs b/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs index 99f6c12de..474a5c1c5 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Utilities/DevDriveUtil.cs @@ -104,7 +104,7 @@ public static bool IsDevDriveFeatureEnabled } catch (Exception ex) { - Log.Error($"Unable to query for Dev Drive enablement: {ex.Message}"); + Log.Error(ex, $"Unable to query for Dev Drive enablement: {ex.Message}"); return false; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs index 113296efd..e8b80f933 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/AddRepoViewModel.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -339,6 +340,28 @@ public async Task ShowCustomizeDevDriveWindow() ToggleCloneButton(); } + [RelayCommand] + public void DevDriveCloneLocationChanged() + { + var location = (EditDevDriveViewModel.DevDrive != null) ? EditDevDriveViewModel.GetDriveDisplayName() : string.Empty; + + if (!string.IsNullOrEmpty(location)) + { + SaveCloneLocation(location); + } + } + + [RelayCommand] + public void SaveCloneLocation(string location) + { + // In cases where location is empty don't update the cloneLocation. Only update when there are actual values. + FolderPickerViewModel.CloneLocation = location; + + FolderPickerViewModel.ValidateCloneLocation(); + + ToggleCloneButton(); + } + /// <summary> /// Indicates if the ListView is currently filtering items. A result of manually filtering a list view /// is that the SelectionChanged is fired for any selected item that is removed and the item isn't "re-selected" @@ -574,6 +597,14 @@ private async Task AddAccountClicked() } } + [RelayCommand] + public void SaveRepoUrl(string repoUrl) + { + Url = repoUrl; + + ToggleCloneButton(); + } + /// <summary> /// Filters all repos down to any that start with text. /// A side-effect of filtering is that SelectionChanged fires for every selected repo but only on removal. @@ -706,7 +737,7 @@ public void GetExtensions() var extensions = extensionWrappers.Where( extension => extension.HasProviderType(ProviderType.Repository) && - extension.HasProviderType(ProviderType.DeveloperId)).OrderBy(extensionWrapper => extensionWrapper.Name); + extension.HasProviderType(ProviderType.DeveloperId)).OrderBy(extensionWrapper => extensionWrapper.ExtensionDisplayName); _providers = new RepositoryProviders(extensions); @@ -1014,8 +1045,8 @@ public void AddOrRemoveRepository(string accountName, IList<object> repositories cloningInformation.RepositoryProvider = _providers.GetSDKProvider(_selectedRepoProvider); cloningInformation.ProviderName = _providers.DisplayName(_selectedRepoProvider); cloningInformation.OwningAccount = developerId; - cloningInformation.EditClonePathAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageEditClonePathAutomationProperties, $"{_selectedRepoProvider}/{repositoryToAdd}"); - cloningInformation.RemoveFromCloningAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageRemoveRepoAutomationProperties, $"{_selectedRepoProvider}/{repositoryToAdd}"); + cloningInformation.EditClonePathAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageEditClonePathAutomationProperties, Path.Join(_selectedRepoProvider, repositoryToAdd.RepoDisplayName)); + cloningInformation.RemoveFromCloningAutomationName = _stringResource.GetLocalized(StringResourceKey.RepoPageRemoveRepoAutomationProperties, Path.Join(_selectedRepoProvider, repositoryToAdd.RepoDisplayName)); EverythingToClone.Add(cloningInformation); } } @@ -1051,7 +1082,7 @@ private void ValidateUriAndChangeUiIfBad(string url, out Uri uri) } catch (Exception e) { - _log.Error($"Invalid URL {uri.OriginalString}", e); + _log.Error(e, $"Invalid URL {uri.OriginalString}"); UrlParsingError = _stringResource.GetLocalized(StringResourceKey.UrlValidationBadUrl); ShouldShowUrlError = true; return; @@ -1248,7 +1279,7 @@ private async Task InitiateAddAccountUserExperienceAsync(RepositoryProvider prov } catch (Exception ex) { - _log.Error($"Exception thrown while calling show logon session", ex); + _log.Error(ex, $"Exception thrown while calling show logon session"); } } } @@ -1335,7 +1366,7 @@ private async Task CoordinateTasks(string loginId, Task<RepositorySearchInformat } catch (Exception ex) { - _log.Error($"Exception thrown while selecting repositories from the return object", ex); + _log.Error(ex, $"Exception thrown while selecting repositories from the return object"); _allRepositories = new(); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationFileViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationFileViewModel.cs index 998cf9136..597e72c0f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationFileViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ConfigurationFileViewModel.cs @@ -89,7 +89,7 @@ private async Task ConfigureAsAdminAsync() } catch (Exception e) { - _log.Error($"Failed to initialize elevated process.", e); + _log.Error(e, $"Failed to initialize elevated process."); } } @@ -113,7 +113,7 @@ private async Task OnLoadedAsync() } catch (Exception e) { - _log.Error($"Failed to get configuration unit details.", e); + _log.Error(e, $"Failed to get configuration unit details."); } } @@ -134,7 +134,7 @@ public async Task<bool> PickConfigurationFileAsync() } catch (Exception e) { - _log.Error($"Failed to open file picker.", e); + _log.Error(e, $"Failed to open file picker."); return false; } } @@ -184,7 +184,7 @@ private async Task<bool> LoadConfigurationFileInternalAsync(StorageFile file) } catch (OpenConfigurationSetException e) { - _log.Error($"Opening configuration set failed.", e); + _log.Error(e, $"Opening configuration set failed."); await _mainWindow.ShowErrorMessageDialogAsync( StringResource.GetLocalized(StringResourceKey.ConfigurationViewTitle, file.Name), GetErrorMessage(e), @@ -192,7 +192,7 @@ await _mainWindow.ShowErrorMessageDialogAsync( } catch (Exception e) { - _log.Error($"Unknown error while opening configuration set.", e); + _log.Error(e, $"Unknown error while opening configuration set."); await _mainWindow.ShowErrorMessageDialogAsync( file.Name, diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs index 48cc57a03..8f2162075 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/DevDriveViewModel.cs @@ -269,7 +269,7 @@ public async Task ChooseFolderLocationAsync() } catch (Exception e) { - _log.Error("Failed to open folder picker.", e); + _log.Error(e, "Failed to open folder picker."); } } @@ -509,7 +509,7 @@ private void RefreshDriveLetterToSizeMapping() } catch (Exception ex) { - _log.Error($"Failed to refresh the drive letter to size mapping.", ex); + _log.Error(ex, $"Failed to refresh the drive letter to size mapping."); // Clear the mapping since it can't be refreshed. This shouldn't happen unless DriveInfo.GetDrives() fails. In that case we won't know which drive // in the list is causing GetDrives()'s to throw. If there are values inside the dictionary at this point, they could be stale. Clearing the list diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemProviderViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemProviderViewModel.cs new file mode 100644 index 000000000..10719a3ce --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemProviderViewModel.cs @@ -0,0 +1,45 @@ +// 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; +using DevHome.Common.Environments.Models; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media.Imaging; + +namespace DevHome.SetupFlow.ViewModels.Environments; + +public partial class ComputeSystemProviderViewModel : ObservableObject +{ + private readonly string _packageFullName; + + public ComputeSystemProviderDetails ProviderDetails { get; private set; } + + [ObservableProperty] + private string _displayName; + + [ObservableProperty] + private ImageIcon _icon; + + [ObservableProperty] + private bool _isSelected; + + public ComputeSystemProviderViewModel(ComputeSystemProviderDetails providerDetails) + { + ProviderDetails = providerDetails; + _packageFullName = ProviderDetails.ExtensionWrapper.PackageFullName; + Icon = GetImageIcon(); + DisplayName = ProviderDetails.ComputeSystemProvider.DisplayName; + } + + private ImageIcon GetImageIcon() + { + var imageIcon = new ImageIcon(); + imageIcon.Source = CardProperty.ConvertMsResourceToIcon(ProviderDetails.ComputeSystemProvider.Icon, _packageFullName); + return imageIcon; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs index a50b4f139..123d84306 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/ComputeSystemsListViewModel.cs @@ -146,7 +146,7 @@ public void FilterComputeSystemCards(string text) } catch (Exception ex) { - _log.Error($"Failed to filter Compute system cards. Error: {ex.Message}"); + _log.Error(ex, $"Failed to filter Compute system cards. Error: {ex.Message}"); } return true; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs new file mode 100644 index 000000000..543ef7d54 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/CreateEnvironmentReviewViewModel.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.SetupFlow.Services; + +namespace DevHome.SetupFlow.ViewModels.Environments; + +public partial class CreateEnvironmentReviewViewModel : ReviewTabViewModelBase +{ + private readonly ISetupFlowStringResource _stringResource; + + public override bool HasItems => true; + + public CreateEnvironmentReviewViewModel( + ISetupFlowStringResource stringResource) + { + _stringResource = stringResource; + TabTitle = stringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewTabTitle); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs new file mode 100644 index 000000000..4a5f1a311 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/EnvironmentCreationOptionsViewModel.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Threading.Tasks; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.DevHomeAdaptiveCards.Parsers; +using DevHome.Common.Environments.Models; +using DevHome.Common.Models; +using DevHome.Common.Renderers; +using DevHome.Common.Services; +using DevHome.SetupFlow.Exceptions; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.Services; +using Microsoft.Windows.DevHome.SDK; +using Serilog; +using WinUIEx; + +namespace DevHome.SetupFlow.ViewModels.Environments; + +/// <summary> +/// View model for the Configure Environment page in the setup flow. This page will display an adaptive card that is provided by the selected +/// compute system provider. The adaptive card will be display in the middle of the page and will contain compute system provider specific UI +/// for the user to configure their creation options. +/// </summary> +public partial class EnvironmentCreationOptionsViewModel : SetupPageViewModelBase, IRecipient<CreationProviderChangedMessage> +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SelectEnvironmentProviderViewModel)); + + private readonly AdaptiveCardRenderingService _adaptiveCardRenderingService; + + private readonly WindowEx _windowEx; + + private readonly AdaptiveElementParserRegistration _elementRegistration = new(); + + private readonly AdaptiveActionParserRegistration _actionRegistration = new(); + + private readonly SetupFlowViewModel _setupFlowViewModel; + + private ComputeSystemProviderDetails _curProviderDetails; + + private AdaptiveCardRenderer _adaptiveCardRenderer; + + private ComputeSystemProviderDetails _upcomingProviderDetails; + + private ExtensionAdaptiveCardSession _extensionAdaptiveCardSession; + + private ExtensionAdaptiveCard _extensionAdaptiveCard; + + private RenderedAdaptiveCard _renderedAdaptiveCard; + + private AdaptiveInputs _userInputsFromAdaptiveCard; + + [ObservableProperty] + private bool _isAdaptiveCardSessionLoaded; + + [ObservableProperty] + private string _sessionErrorMessage; + + public EnvironmentCreationOptionsViewModel( + ISetupFlowStringResource stringResource, + SetupFlowOrchestrator orchestrator, + SetupFlowViewModel setupFlow, + WindowEx windowEx, + AdaptiveCardRenderingService renderingService) + : base(stringResource, orchestrator) + { + PageTitle = stringResource.GetLocalized(StringResourceKey.ConfigureEnvironmentPageTitle); + _setupFlowViewModel = setupFlow; + _setupFlowViewModel.EndSetupFlow += OnEndSetupFlow; + _windowEx = windowEx; + + // Register for changes to the selected provider. This will be triggered when the user selects a provider. + // from the SelectEnvironmentProviderViewModel. This is a weak reference so that the recipient can be garbage collected. + WeakReferenceMessenger.Default.Register<CreationProviderChangedMessage>(this); + + // Register to receive the EnvironmentCreationOptionsViewRequestMessage so that we can populate the configure environment page with the current + // adaptive card information. This handles the case where the view is loaded after the view model has finished loading the adaptive card. + WeakReferenceMessenger.Default.Register<EnvironmentCreationOptionsViewModel, CreationOptionsViewPageRequestMessage>(this, OnEnvironmentOptionsViewRequest); + + WeakReferenceMessenger.Default.Register<EnvironmentCreationOptionsViewModel, CreationOptionsReviewPageDataRequestMessage>(this, OnReviewPageViewRequest); + + // register the supported element parsers + _elementRegistration.Set(DevHomeSettingsCard.AdaptiveElementType, new DevHomeSettingsCardParser()); + _elementRegistration.Set(DevHomeSettingsCardChoiceSet.AdaptiveElementType, new DevHomeSettingsCardChoiceSetParser()); + _elementRegistration.Set(DevHomeLaunchContentDialogButton.AdaptiveElementType, new DevHomeLaunchContentDialogButtonParser()); + _elementRegistration.Set(DevHomeContentDialogContent.AdaptiveElementType, new DevHomeContentDialogContentParser()); + _adaptiveCardRenderingService = renderingService; + Orchestrator.CurrentSetupFlowKind = SetupFlowKind.CreateEnvironment; + } + + /// <summary> + /// Weak reference message handler for when the selected provider changes in the select environment provider page. This will be triggered when the user + /// selects an item in the Select Environment Provider page. The next time the user goes to this configure environments page, we'll update the UI + /// with an adaptive card from the newly selected provider. + /// </summary> + /// <param name="message">Message data that contains the new provider details.</param> + public void Receive(CreationProviderChangedMessage message) + { + _upcomingProviderDetails = message.Value; + } + + private void OnEndSetupFlow(object sender, EventArgs e) + { + ResetAdaptiveCardConfiguration(); + WeakReferenceMessenger.Default.UnregisterAll(this); + _setupFlowViewModel.EndSetupFlow -= OnEndSetupFlow; + Orchestrator.CurrentSetupFlowKind = SetupFlowKind.LocalMachine; + } + + protected async override Task OnFirstNavigateToAsync() + { + _adaptiveCardRenderer = await GetAdaptiveCardRenderer(); + } + + /// <summary> + /// Make sure we only get the list of ComputeSystems from the ComputeSystemManager once when the page is first navigated to. + /// All other times will be through the use of the sync button. + /// </summary> + protected async override Task OnEachNavigateToAsync() + { + await Task.CompletedTask; + + var curSelectedProviderId = _curProviderDetails?.ComputeSystemProvider?.Id ?? string.Empty; + var upcomingSelectedProviderId = _upcomingProviderDetails?.ComputeSystemProvider?.Id; + + // Selected compute system provider may havechanged so we need to update the adaptive card in the UI + // with new a adaptive card from the new provider. + _curProviderDetails = _upcomingProviderDetails; + + IsAdaptiveCardSessionLoaded = false; + + // Its possible that an extension could take a long time to load the adaptive card session. + // So we run this on a background thread to prevent the UI from freezing. + _ = Task.Run(() => + { + var developerIdWrapper = _curProviderDetails.DeveloperIds.First(); + var result = _curProviderDetails.ComputeSystemProvider.CreateAdaptiveCardSessionForDeveloperId(developerIdWrapper.DeveloperId, ComputeSystemAdaptiveCardKind.CreateComputeSystem); + UpdateExtensionAdaptiveCard(result); + }); + } + + /// <summary> + /// Gets and configures the adaptive card that will be displayed on the configure environment page. + /// </summary> + public void UpdateExtensionAdaptiveCard(ComputeSystemAdaptiveCardResult adaptiveCardSessionResult) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + try + { + CanGoToNextPage = false; + + // Reset error state and remove event handler from previous session. + ResetAdaptiveCardConfiguration(); + + if (adaptiveCardSessionResult.Result.Status == ProviderOperationStatus.Failure) + { + _log.Error($"{adaptiveCardSessionResult.Result.DisplayMessage} - {adaptiveCardSessionResult.Result.DiagnosticText}"); + throw new AdaptiveCardNotRetrievedException(adaptiveCardSessionResult.Result.DisplayMessage); + } + + // Create a new adaptive card session wrapper and add event handlers for when the session stops. + _extensionAdaptiveCardSession = new ExtensionAdaptiveCardSession(adaptiveCardSessionResult.ComputeSystemCardSession); + _extensionAdaptiveCardSession.Stopped += OnAdaptiveCardSessionStopped; + + // Create the Dev Home sdk extension adaptive card with our custom element and action parsers and send + // it to the extension who will update the card with an adaptive card template and data for the template. + // We use the OnAdaptiveCardUpdated method to update Dev Home's UI when IExtensionAdaptiveCard.Update is called. + _extensionAdaptiveCard = new ExtensionAdaptiveCard(_elementRegistration, _actionRegistration); + _extensionAdaptiveCard.UiUpdate += OnAdaptiveCardUpdated; + + // Initialize the adaptive card session with the extension adaptive card template and data with an initial + // call to IExtensionAdaptiveCard.Update. + _extensionAdaptiveCardSession.Initialize(_extensionAdaptiveCard); + } + catch (Exception ex) + { + _log.Error(ex, $"Failed to get creation options adaptive card from provider {_curProviderDetails.ComputeSystemProvider.Id}."); + SessionErrorMessage = ex.Message; + } + }); + } + + /// <summary> + /// When the <see cref="DevHome.Common.Models.ExtensionAdaptiveCard"/> is updated by the extension we need to render the new adaptive card in the UI. + /// This method does the work needed to create an adaptive card renderer, render the adaptive card and send the new adaptive card + /// any view that is listening for the <see cref="NewAdaptiveCardAvailableMessage"/> message. + /// </summary> + public void OnAdaptiveCardUpdated(object sender, AdaptiveCard adaptiveCard) + { + _windowEx.DispatcherQueue.TryEnqueue(() => + { + // Render the adaptive card and set the action event handler. + _renderedAdaptiveCard = _adaptiveCardRenderer.RenderAdaptiveCard(adaptiveCard); + _renderedAdaptiveCard.Action += OnRenderedAdaptiveCardAction; + + // Send new card to listeners + _userInputsFromAdaptiveCard = _renderedAdaptiveCard.UserInputs; + WeakReferenceMessenger.Default.Send(new NewAdaptiveCardAvailableMessage(new RenderedAdaptiveCardData(Orchestrator.CurrentPageViewModel, _renderedAdaptiveCard))); + IsAdaptiveCardSessionLoaded = true; + + // We set CanGoToNextPage to true here because we can only validate the inputs when the user interacts with the adaptive card + // via the action buttons. + CanGoToNextPage = true; + }); + } + + /// <summary> + /// When the user interacts with the adaptive card by clicking the next or previous buttons in the Setup flow, we need to send + /// the inputs and actions back to the extension. The extension will then process the inputs and actions and update the adaptive card + /// Which will ultimately cause the <see cref="OnAdaptiveCardUpdated"/> method to be called. + /// </summary> + /// <param name="sender">The rendered adaptive card whose submite or execute action was just invoked </param> + /// <param name="args">The action and user inputs from within the adaptive card</param> + private void OnRenderedAdaptiveCardAction(object sender, AdaptiveActionEventArgs args) + { + _windowEx.DispatcherQueue.TryEnqueue(async () => + { + IsAdaptiveCardSessionLoaded = false; + + // Send the inputs and actions that the user entered back to the extension. + await _extensionAdaptiveCardSession.OnAction(args.Action.ToJson().Stringify(), args.Inputs.AsJson().Stringify()); + }); + } + + private void ResetAdaptiveCardConfiguration() + { + SessionErrorMessage = null; + if (_extensionAdaptiveCardSession != null) + { + _extensionAdaptiveCardSession.Stopped -= OnAdaptiveCardSessionStopped; + } + + if (_extensionAdaptiveCard != null) + { + _extensionAdaptiveCard.UiUpdate -= OnAdaptiveCardUpdated; + } + + if (_renderedAdaptiveCard != null) + { + _renderedAdaptiveCard.Action -= OnRenderedAdaptiveCardAction; + } + } + + /// <summary> + /// The configure environment view page will request an adaptive card to display in the UI if it loads after the extension sends out the CreationOptionsViewPageRequestMessage. + /// </summary> + /// <param name="recipient">The class that should be receiving the request</param> + /// <param name="message">The payload of the message request</param> + private void OnEnvironmentOptionsViewRequest(EnvironmentCreationOptionsViewModel recipient, CreationOptionsViewPageRequestMessage message) + { + message.Reply(_renderedAdaptiveCard); + } + + /// <summary> + /// The review environments view / summary page view will request an adaptive card to display in the UI if it loads after this view model sends out the original RenderedAdaptiveCard message. + /// this can happen when the user navigates away from the review page to another page in Dev Home. E.g the settings page, then navigates back to the review page. At this point the review + /// page is unloaded when the user navigates away from it. When they navigate back to it, a new view will be created and loaded, so we need to request the adaptive again from this view model. + /// </summary> + /// <param name="recipient">The class that should be receiving the request</param> + /// <param name="message">The payload of the message request</param> + private void OnReviewPageViewRequest(EnvironmentCreationOptionsViewModel recipient, CreationOptionsReviewPageDataRequestMessage message) + { + // Only send the adaptive card if the session has loaded. If the session hasn't loaded yet, we'll send an empty response. The review page should be sent the adaptive card + // once the session has loaded in the OnAdaptiveCardUpdated method. + if (!IsAdaptiveCardSessionLoaded && Orchestrator?.CurrentPageViewModel is not SummaryViewModel) + { + return; + } + + message.Reply(_renderedAdaptiveCard); + } + + /// <summary> + /// When the extension indicates that the session has stopped, we need to get the result json from the session. Once we get this + /// we can send a message to the CreateEnvironmentTask to let it know that the adaptive card session has ended. + /// It will then update its setup tasks with information to create the compute system. + /// </summary> + /// <param name="sender">The extension session object who stopped the session</param> + /// <param name="args">Data payload that contains the users provided input</param> + private void OnAdaptiveCardSessionStopped(ExtensionAdaptiveCardSession sender, ExtensionAdaptiveCardSessionStoppedEventArgs args) + { + // Send message to the CreateEnvironmentTask to let it know that the adaptive card session has ended. + // the task will use the ResultJson to create the compute system. + WeakReferenceMessenger.Default.Send(new CreationAdaptiveCardSessionEndedMessage(new CreationAdaptiveCardSessionEndedData(args.ResultJson, _curProviderDetails))); + sender.Stopped -= OnAdaptiveCardSessionStopped; + _extensionAdaptiveCard.UiUpdate -= OnAdaptiveCardUpdated; + } + + /// <summary> + /// Gets the adaptive card renderer that will be used to render the adaptive card in the UI. Its important to recreate the ItemsViewChoiceSet every time we want to + /// render an adaptive card because the parenting the ItemsView control to multiple parents will cause an exception to be thrown. + /// </summary> + private async Task<AdaptiveCardRenderer> GetAdaptiveCardRenderer() + { + var renderer = await _adaptiveCardRenderingService.GetRendererAsync(); + renderer.ElementRenderers.Set(DevHomeSettingsCardChoiceSet.AdaptiveElementType, new ItemsViewChoiceSet("SettingsCardWithButtonThatLaunchesContentDialog")); + + // We need to keep the same renderer for the ActionSet that is hooked up to the orchestrator as it will have the adaptive card + // context needed to invoke the adaptive card actions from outside the adaptive card. + renderer.ElementRenderers.Set("ActionSet", Orchestrator.DevHomeActionSetRenderer); + return renderer; + } + + /// <inheritdoc cref="SetupPageViewModelBase.GetAdaptiveCardUserInputs"/> + protected override AdaptiveInputs GetAdaptiveCardUserInputs() + { + return _userInputsFromAdaptiveCard; + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs new file mode 100644 index 000000000..dedd93a42 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/Environments/SelectEnvironmentProviderViewModel.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.Common.Contracts.Services; +using DevHome.Common.Environments.Models; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.Services; +using Serilog; +using WinUIEx; + +namespace DevHome.SetupFlow.ViewModels.Environments; + +public partial class SelectEnvironmentProviderViewModel : SetupPageViewModelBase +{ + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(SelectEnvironmentProviderViewModel)); + + private readonly IComputeSystemService _computeSystemService; + + public ComputeSystemProviderDetails SelectedProvider { get; private set; } + + [ObservableProperty] + private bool _areProvidersLoaded; + + [ObservableProperty] + private int _selectedProviderIndex; + + [ObservableProperty] + private ObservableCollection<ComputeSystemProviderViewModel> _providersViewModels; + + public SelectEnvironmentProviderViewModel( + ISetupFlowStringResource stringResource, + SetupFlowOrchestrator orchestrator, + IComputeSystemService computeSystemService) + : base(stringResource, orchestrator) + { + PageTitle = stringResource.GetLocalized(StringResourceKey.SelectEnvironmentPageTitle); + _computeSystemService = computeSystemService; + } + + private async Task LoadProvidersAsync() + { + AreProvidersLoaded = false; + Orchestrator.NotifyNavigationCanExecuteChanged(); + + var providerDetails = await Task.Run(_computeSystemService.GetComputeSystemProvidersAsync); + ProvidersViewModels = new(); + foreach (var providerDetail in providerDetails) + { + ProvidersViewModels.Add(new ComputeSystemProviderViewModel(providerDetail)); + } + + AreProvidersLoaded = true; + } + + protected async override Task OnFirstNavigateToAsync() + { + CanGoToNextPage = false; + await LoadProvidersAsync(); + } + + [RelayCommand] + private void ItemsViewSelectionChanged(ComputeSystemProviderViewModel sender) + { + if (sender != null) + { + // When navigating between the select providers page and the configure creation options page + // visual selection is lost, so we need deselect the providers first. Then select correct one. + // this will ensure that the correct provider is visually selected when navigating back to the select providers page. + foreach (var provider in ProvidersViewModels) + { + provider.IsSelected = false; + } + + sender.IsSelected = true; + SelectedProvider = sender.ProviderDetails; + + // Using the default channel to send the message to the recipient. In this case, the EnvironmentCreationOptionsViewModel. + // In the future if we support a multi-instance setup flow, we can use a custom channel/a message broker to send messages. + // For now, we are using the default channel. + WeakReferenceMessenger.Default.Send(new CreationProviderChangedMessage(SelectedProvider)); + CanGoToNextPage = true; + Orchestrator.NotifyNavigationCanExecuteChanged(); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs index 9d7999dfd..f4e5b397c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/FolderPickerViewModel.cs @@ -10,7 +10,6 @@ using DevHome.SetupFlow.Services; using Microsoft.UI.Xaml; using Serilog; -using Windows.Storage.Pickers; using WinUIEx; namespace DevHome.SetupFlow.ViewModels; @@ -146,7 +145,7 @@ private async Task<DirectoryInfo> PickCloneDirectoryAsync() } catch (Exception e) { - _log.Error("Failed to open folder picker", e); + _log.Error(e, "Failed to open folder picker"); return null; } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs index 18a5c5c17..76a531986 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/MainPageViewModel.cs @@ -34,6 +34,8 @@ public partial class MainPageViewModel : SetupPageViewModelBase private const string EnvironmentsSetupFlowFeatureName = "EnvironmentsSetupTargetFlow"; + private const string EnvironmentsCreationFlowFeatureName = "EnvironmentsCreationFlow"; + private readonly IHost _host; private readonly IWindowsPackageManager _wpm; private readonly IDesiredStateConfiguration _dsc; @@ -59,6 +61,8 @@ public partial class MainPageViewModel : SetupPageViewModelBase public bool ShouldShowSetupTargetItem => _experimentationService.IsFeatureEnabled(EnvironmentsSetupFlowFeatureName); + public bool ShouldShowCreateEnvironmentItem => _experimentationService.IsFeatureEnabled(EnvironmentsCreationFlowFeatureName); + /// <summary> /// Event raised when the user elects to start the setup flow. /// The orchestrator for the whole flow subscribes to this event to handle @@ -184,6 +188,20 @@ private void StartRepoConfig(string flowTitle) _host.GetService<DevDriveTaskGroup>()); } + /// <summary> + /// Starts the create environment flow. + /// </summary> + [RelayCommand] + public void StartCreateEnvironment(string flowTitle) + { + _log.Information("Starting flow for environment creation"); + StartSetupFlowForTaskGroups( + flowTitle, + "CreateEnvironment", + _host.GetService<SelectEnvironmentProviderTaskGroup>(), + _host.GetService<EnvironmentCreationOptionsTaskGroup>()); + } + /// <summary> /// Starts a setup flow that only includes app management. /// </summary> diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs index 693271087..f8728f372 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/PackageCatalogListViewModel.cs @@ -93,7 +93,7 @@ private async Task LoadCatalogsAsync() } catch (Exception e) { - _log.Error($"Failed to load catalogs.", e); + _log.Error(e, $"Failed to load catalogs."); } finally { diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs index 7f9ef63fa..8926e1b55 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/ReviewViewModel.cs @@ -41,6 +41,15 @@ public partial class ReviewViewModel : SetupPageViewModelBase [NotifyCanExecuteChangedFor(nameof(SetUpCommand))] private bool _canSetUp; + [ObservableProperty] + private string _reviewPageTitle; + + [ObservableProperty] + private string _reviewPageExpanderDescription; + + [ObservableProperty] + private string _reviewPageDescription; + public bool HasApplicationsToInstall => Orchestrator.GetTaskGroup<AppManagementTaskGroup>()?.SetupTasks.Any() == true; public bool RequiresTermsAgreement => HasApplicationsToInstall; @@ -88,6 +97,8 @@ public ReviewViewModel( { NextPageButtonText = StringResource.GetLocalized(StringResourceKey.SetUpButton); PageTitle = StringResource.GetLocalized(StringResourceKey.ReviewPageTitle); + ReviewPageExpanderDescription = StringResource.GetLocalized(StringResourceKey.ReviewExpanderDescription); + ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescription); _setupFlowOrchestrator = orchestrator; _configFileBuilder = configFileBuilder; @@ -104,7 +115,18 @@ protected async override Task OnEachNavigateToAsync() .ToList(); SelectedReviewTab = ReviewTabs.FirstOrDefault(); + // If the CreateEnvironmentTaskGroup is present, update the setup button text to "Create Environment" + // and page title to "Review your environment" + if (Orchestrator.GetTaskGroup<EnvironmentCreationOptionsTaskGroup>() != null) + { + NextPageButtonText = StringResource.GetLocalized(StringResourceKey.CreateEnvironmentButtonText); + PageTitle = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewPageTitle); + ReviewPageExpanderDescription = StringResource.GetLocalized(StringResourceKey.EnvironmentCreationReviewExpanderDescription); + ReviewPageDescription = StringResource.GetLocalized(StringResourceKey.SetupShellReviewPageDescriptionForEnvironmentCreation); + } + NextPageButtonToolTipText = HasTasksToSetUp ? null : StringResource.GetLocalized(StringResourceKey.ReviewNothingToSetUpToolTip); + UpdateCanSetUp(); await Task.CompletedTask; @@ -142,7 +164,7 @@ private async Task OnSetUpAsync() } catch (Exception e) { - _log.Error($"Failed to initialize elevated process.", e); + _log.Error(e, $"Failed to initialize elevated process."); } } @@ -166,7 +188,7 @@ private async Task DownloadConfigurationAsync() } catch (Exception e) { - _log.Error($"Failed to download configuration file.", e); + _log.Error(e, $"Failed to download configuration file."); } } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs index 5cb8bfd16..a80727ff6 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SearchViewModel.cs @@ -128,7 +128,7 @@ public SearchViewModel(IWindowsPackageManager wpm, ISetupFlowStringResource stri } catch (Exception e) { - _log.Error($"Search error.", e); + _log.Error(e, $"Search error."); return (SearchResultStatus.ExceptionThrown, null); } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs index f6f3b2354..feba7c037 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupFlowViewModel.cs @@ -14,6 +14,7 @@ using DevHome.SetupFlow.Services; using DevHome.Telemetry; using Microsoft.Extensions.Hosting; +using Microsoft.UI.Xaml.Navigation; using Serilog; using Windows.Storage; @@ -26,6 +27,8 @@ public partial class SetupFlowViewModel : ObservableObject private readonly MainPageViewModel _mainPageViewModel; private readonly PackageProvider _packageProvider; + private readonly string _creationFlowNavigationParameter = "StartCreationFlow"; + public SetupFlowOrchestrator Orchestrator { get; } public event EventHandler EndSetupFlow = (s, e) => { }; @@ -119,4 +122,25 @@ public async Task StartFileActivationFlowAsync(StorageFile file) Orchestrator.FlowPages = [_mainPageViewModel]; await _mainPageViewModel.StartConfigurationFileAsync(file); } + + public void StartCreationFlowAsync() + { + Orchestrator.FlowPages = [_mainPageViewModel]; + _mainPageViewModel.StartCreateEnvironment(string.Empty); + } + + 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)) && + _creationFlowNavigationParameter.Equals(parameter, StringComparison.OrdinalIgnoreCase) && + Orchestrator.CurrentSetupFlowKind != SetupFlowKind.CreateEnvironment) + { + Cancel(); + StartCreationFlowAsync(); + } + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs index d9b669bb3..cc36f1b53 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupPageViewModelBase.cs @@ -3,7 +3,11 @@ using System.Linq; using System.Threading.Tasks; +using AdaptiveCards.ObjectModel.WinUI3; +using AdaptiveCards.Rendering.WinUI3; using CommunityToolkit.Mvvm.ComponentModel; +using DevHome.Common.DevHomeAdaptiveCards.CardModels; +using DevHome.Common.DevHomeAdaptiveCards.Parsers; using DevHome.Common.TelemetryEvents.SetupFlow; using DevHome.SetupFlow.Services; using DevHome.Telemetry; @@ -189,4 +193,26 @@ protected async virtual Task OnFirstNavigateFromAsync() // Do nothing await Task.CompletedTask; } + + /// <summary> + /// Hook so the orchestrator can validate if the user can navigate to the next page when a page is rendering + /// an adaptive card that is hooked up to the <see cref="SetupFlowOrchestrator.DevHomeActionSetRenderer"/>. + /// </summary> + /// <remarks> + /// The orchestrator takes care of calling this when appropriate through <see cref="GetAdaptiveCardUserInputsForNavigationValidation"/>. + /// This runs on the UI thread, but the cost of validating the inputs should be minimal. + /// </remarks> + protected virtual AdaptiveInputs GetAdaptiveCardUserInputs() + { + return new AdaptiveInputs(); + } + + /// <summary> + /// Performs the work to validate the user inputs when navigating to the next page when the page is rendering an adaptive card. + /// </summary> + /// <returns>The adaptive card inputs for the adaptive card currently presented to the user on the setup flow page</returns> + public AdaptiveInputs GetAdaptiveCardUserInputsForNavigationValidation() + { + return GetAdaptiveCardUserInputs(); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs index 44ed8eabb..80078db0f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SetupTargetViewModel.cs @@ -196,7 +196,7 @@ private bool ShouldShowListInUI(ComputeSystemsListViewModel listViewModel, strin } catch (Exception ex) { - _log.Error($"Error filtering ComputeSystemsListViewModel", ex); + _log.Error(ex, $"Error filtering ComputeSystemsListViewModel"); } return true; @@ -366,7 +366,7 @@ public async Task LoadAllComputeSystemsInTheUI() } catch (Exception ex) { - _log.Error($"Error loading ComputeSystemViewModels data", ex); + _log.Error(ex, $"Error loading ComputeSystemViewModels data"); } ShouldShowShimmerBelowList = false; diff --git a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs index 884831843..b6e45248d 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/ViewModels/SummaryViewModel.cs @@ -108,6 +108,20 @@ public ObservableCollection<PackageViewModel> AppsDownloaded } } + public bool WasCreateEnvironmentOperationStarted + { + get + { + var taskGroup = Orchestrator.GetTaskGroup<EnvironmentCreationOptionsTaskGroup>(); + if (taskGroup == null) + { + return false; + } + + return taskGroup.CreateEnvironmentTask.CreationOperationStarted; + } + } + public List<PackageViewModel> AppsDownloadedInstallationNotes => AppsDownloaded.Where(p => !string.IsNullOrEmpty(p.InstallationNotes)).ToList(); public IList<ConfigurationUnitResultViewModel> ConfigurationUnitResults => _configurationUnitResults.Value; @@ -134,6 +148,9 @@ public ObservableCollection<PackageViewModel> AppsDownloaded [ObservableProperty] private string _applicationsClonedText; + [ObservableProperty] + private string _summaryPageEnvironmentCreatingText; + [RelayCommand] public void RemoveRestartGrid() { @@ -157,7 +174,6 @@ public void GoToMainPage() _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToMainPage"); } - [RelayCommand] public void GoToDashboard() { TelemetryFactory.Get<ITelemetry>().Log("Summary_NavigateTo_Event", LogLevel.Critical, new NavigateFromSummaryEvent("Dashboard"), Orchestrator.ActivityId); @@ -165,6 +181,26 @@ public void GoToDashboard() _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToDashboard"); } + [RelayCommand] + public void RedirectToNextPage() + { + if (WasCreateEnvironmentOperationStarted) + { + GoToEnvironmentsPage(); + return; + } + + // Default behavior is to go to the dashboard + GoToDashboard(); + } + + public void GoToEnvironmentsPage() + { + TelemetryFactory.Get<ITelemetry>().Log("Summary_NavigateTo_Event", LogLevel.Critical, new NavigateFromSummaryEvent("Environments"), Orchestrator.ActivityId); + _host.GetService<INavigationService>().NavigateTo(KnownPageKeys.Environments); + _setupFlowViewModel.TerminateCurrentFlow("Summary_GoToEnvironments"); + } + [RelayCommand] public void GoToDevHomeSettings() { @@ -180,6 +216,12 @@ public void GoToForDevelopersSettingsPage() Task.Run(() => Launcher.LaunchUriAsync(new Uri("ms-settings:developers"))).Wait(); } + [ObservableProperty] + private string _pageRedirectButtonText; + + [ObservableProperty] + private string _pageHeaderText; + public SummaryViewModel( ISetupFlowStringResource stringResource, SetupFlowOrchestrator orchestrator, @@ -199,6 +241,8 @@ public SummaryViewModel( _showRestartNeeded = Visibility.Collapsed; _appManagementInitializer = appManagementInitializer; _cloneRepoNextSteps = new(); + PageRedirectButtonText = StringResource.GetLocalized(StringResourceKey.SummaryPageOpenDashboard); + PageHeaderText = StringResource.GetLocalized(StringResourceKey.SummaryPageHeader); IsNavigationBarVisible = true; IsStepPage = false; @@ -259,6 +303,12 @@ protected async override Task OnFirstNavigateToAsync() TelemetryFactory.Get<ITelemetry>().LogCritical("Summary_NavigatedTo_Event", false, Orchestrator.ActivityId); } + if (WasCreateEnvironmentOperationStarted) + { + PageRedirectButtonText = StringResource.GetLocalized(StringResourceKey.SummaryPageRedirectToEnvironmentPageButton); + PageHeaderText = StringResource.GetLocalized(StringResourceKey.SummaryPageHeaderForEnvironmentCreationText); + } + await ReloadCatalogsAsync(); } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml index 06639efba..ce148ff92 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml @@ -61,9 +61,14 @@ <TextBox x:Name="RepoUrlTextBox" x:Uid="RepoUrl" Visibility="{x:Bind AddRepoViewModel.ShowUrlPage, Mode=OneWay}" - TextChanged="RepoUrlTextBox_TextChanged" Text="{x:Bind AddRepoViewModel.Url, Mode=TwoWay}" - Margin="0, 20, 0, 0"/> + Margin="0, 20, 0, 0"> + <i:Interaction.Behaviors> + <ic:EventTriggerBehavior EventName="TextChanged"> + <ic:InvokeCommandAction Command="{x:Bind AddRepoViewModel.SaveRepoUrlCommand}" CommandParameter="{x:Bind RepoUrlTextBox.Text, Mode=OneWay}"/> + </ic:EventTriggerBehavior> + </i:Interaction.Behaviors> + </TextBox> <TextBlock x:Name="UrlErrorTextBlock" Style="{ThemeResource BaseTextBlockStyle}" Text="{x:Bind AddRepoViewModel.UrlParsingError, Mode=OneWay}" Visibility="{x:Bind AddRepoViewModel.ShouldShowUrlError, Mode=OneWay}"/> <!-- Log into account --> @@ -211,17 +216,29 @@ <TextBlock x:Uid="ClonePathForTextBlock" /> <TextBox - TextChanged="CloneLocation_TextChanged" Text="{x:Bind AddRepoViewModel.FolderPickerViewModel.CloneLocationAlias, Mode=TwoWay}" IsEnabled="False" Visibility="{x:Bind AddRepoViewModel.FolderPickerViewModel.InDevDriveScenario, Mode=OneWay}" - x:Name="DevDriveCloneLocationAliasTextBox" /> + x:Name="DevDriveCloneLocationAliasTextBox"> + <i:Interaction.Behaviors> + <ic:EventTriggerBehavior EventName="TextChanged"> + <ic:InvokeCommandAction Command="{x:Bind AddRepoViewModel.DevDriveCloneLocationChangedCommand}"/> + </ic:EventTriggerBehavior> + </i:Interaction.Behaviors> + </TextBox> </StackPanel> <TextBox x:Uid="ClonePath" - TextChanged="CloneLocation_TextChanged" Grid.Column="0" + Grid.Column="0" + x:Name="ClonePathTextBox" Text="{x:Bind AddRepoViewModel.FolderPickerViewModel.CloneLocation, Mode=TwoWay}" IsEnabled="{x:Bind AddRepoViewModel.FolderPickerViewModel.IsBrowseButtonEnabled, Mode=OneWay}" - Visibility="{x:Bind AddRepoViewModel.FolderPickerViewModel.InDevDriveScenario, Mode=OneWay, Converter={StaticResource NegatedBoolToVisibilityConverter}}"/> + Visibility="{x:Bind AddRepoViewModel.FolderPickerViewModel.InDevDriveScenario, Mode=OneWay, Converter={StaticResource NegatedBoolToVisibilityConverter}}"> + <i:Interaction.Behaviors> + <ic:EventTriggerBehavior EventName="TextChanged"> + <ic:InvokeCommandAction Command="{x:Bind AddRepoViewModel.SaveCloneLocationCommand}" CommandParameter="{x:Bind ClonePathTextBox.Text, Mode=OneWay}"/> + </ic:EventTriggerBehavior> + </i:Interaction.Behaviors> + </TextBox> <Grid Grid.Column="1"> <!-- Workaround to show a tooltip on a disabled button. https://github.com/microsoft/microsoft-ui-xaml/issues/2262 --> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs index b45115b4f..01e125a4a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/AddRepoDialog.xaml.cs @@ -110,29 +110,6 @@ public void DeveloperIdChangedEventHandler(object sender, IDeveloperId developer } } - /// <summary> - /// Validate the user put in a rooted, non-null path. - /// </summary> - private void CloneLocation_TextChanged(object sender, TextChangedEventArgs e) - { - // just in case something other than a text box calls this. - if (sender is TextBox cloneLocationTextBox) - { - var location = cloneLocationTextBox.Text; - if (string.Equals(cloneLocationTextBox.Name, "DevDriveCloneLocationAliasTextBox", StringComparison.Ordinal)) - { - location = (AddRepoViewModel.EditDevDriveViewModel.DevDrive != null) ? AddRepoViewModel.EditDevDriveViewModel.GetDriveDisplayName() : string.Empty; - } - - // In cases where location is empty don't update the cloneLocation. Only update when there are actual values. - AddRepoViewModel.FolderPickerViewModel.CloneLocation = string.IsNullOrEmpty(location) ? AddRepoViewModel.FolderPickerViewModel.CloneLocation : location; - } - - AddRepoViewModel.FolderPickerViewModel.ValidateCloneLocation(); - - AddRepoViewModel.ToggleCloneButton(); - } - /// <summary> /// If any items in reposToSelect exist in the UI, select them. /// An side-effect of SelectRange is SelectionChanged is fired for each item SelectRange is called on. @@ -306,17 +283,6 @@ private void MakeNewDevDriveCheckBox_Click(object sender, RoutedEventArgs e) } } - private void RepoUrlTextBox_TextChanged(object sender, RoutedEventArgs e) - { - // just in case something other than a text box calls this. - if (sender is TextBox) - { - AddRepoViewModel.Url = (sender as TextBox).Text; - } - - AddRepoViewModel.ToggleCloneButton(); - } - /// <summary> /// Putting the event in the view so SelectRange can be called. /// SelectRange needs a reference to the ListView. diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/EditClonePathDialog.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/EditClonePathDialog.xaml index 002122e47..96f485a7e 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/EditClonePathDialog.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/EditClonePathDialog.xaml @@ -53,7 +53,7 @@ <Grid Visibility="{x:Bind FolderPickerViewModel.ShouldShowFolderPicker, Mode=OneWay}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="5*"/> - <ColumnDefinition Width="*"/> + <ColumnDefinition Width="auto"/> </Grid.ColumnDefinitions> <!--Folder picker--> <StackPanel @@ -83,7 +83,7 @@ x:Uid="ms-resource:///DevHome.SetupFlow/Resources/ClonePath_Button" Click="ChooseCloneLocationButton_Click" Grid.Column="1" - Margin="5, 27, 0, 0" + Margin="5, 27, 10, 0" Padding="8 5 10 5"/> </Grid> </Grid> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml new file mode 100644 index 000000000..8a98075a4 --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<UserControl + x:Class="DevHome.SetupFlow.Views.Environments.CreateEnvironmentReviewView" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + Unloaded="ViewUnloaded" + Loaded="ViewLoaded" + xmlns:converters="using:CommunityToolkit.WinUI.Converters"> + <UserControl.Resources> + <converters:EmptyObjectToObjectConverter x:Key="EmptyObjectToObjectConverter" NotEmptyValue="Visible" EmptyValue="Collapsed"/> + </UserControl.Resources> + + <!-- Xaml for create environment view tab in the create environment flow--> + <Grid> + <ScrollViewer HorizontalScrollBarVisibility="Auto"> + <StackPanel Spacing="10"> + <Grid x:Name="AdaptiveCardGrid" /> + </StackPanel> + </ScrollViewer> + </Grid> +</UserControl> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs new file mode 100644 index 000000000..24ca943eb --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/CreateEnvironmentReviewView.xaml.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.ViewModels; +using DevHome.SetupFlow.ViewModels.Environments; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Serilog; + +namespace DevHome.SetupFlow.Views.Environments; + +public sealed partial class CreateEnvironmentReviewView : UserControl, IRecipient<NewAdaptiveCardAvailableMessage> +{ + // Logging to capture any adaptive card rendering exceptions so the app doesn't crash + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(CreateEnvironmentReviewView)); + + public CreateEnvironmentReviewViewModel ViewModel => (CreateEnvironmentReviewViewModel)this.DataContext; + + public CreateEnvironmentReviewView() + { + this.InitializeComponent(); + WeakReferenceMessenger.Default.Register<NewAdaptiveCardAvailableMessage>(this); + } + + private void ViewUnloaded(object sender, RoutedEventArgs e) + { + AdaptiveCardGrid.Children.Clear(); + WeakReferenceMessenger.Default.UnregisterAll(this); + } + + /// <summary> + /// Recieves the adaptive card from the view model, when the view model finishes loading it. + /// </summary> + public void Receive(NewAdaptiveCardAvailableMessage message) + { + // Only process the message if the view model is the ReviewViewModel + if (message.Value.CurrentSetupFlowViewModel is ReviewViewModel) + { + AddAdaptiveCardToUI(message.Value.RenderedAdaptiveCard); + } + } + + /// <summary> + /// Request the adaptive cad from the view model + /// </summary> + private void ViewLoaded(object sender, RoutedEventArgs e) + { + var message = WeakReferenceMessenger.Default.Send<CreationOptionsReviewPageDataRequestMessage>(); + if (!message.HasReceivedResponse) + { + return; + } + + AddAdaptiveCardToUI(message.Response); + } + + private void AddAdaptiveCardToUI(RenderedAdaptiveCard renderedAdaptiveCard) + { + try + { + var frameworkElement = renderedAdaptiveCard?.FrameworkElement; + if (frameworkElement == null) + { + return; + } + + AdaptiveCardGrid.Children.Clear(); + AdaptiveCardGrid.Children.Add(frameworkElement); + } + catch (Exception ex) + { + // Log the exception + _log.Error(ex, "Error adding adaptive card UI in CreateEnvironmentReviewView"); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml new file mode 100644 index 000000000..fa967365f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<UserControl + x:Class="DevHome.SetupFlow.Views.Environments.EnvironmentCreationOptionsView" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:setupControls="using:DevHome.SetupFlow.Controls" + xmlns:converters="using:CommunityToolkit.WinUI.Converters" + Unloaded="ViewUnloaded" + Loaded="ViewLoaded"> + <UserControl.Resources> + <converters:BoolToVisibilityConverter x:Key="CollapsedWhenTrueBoolToVisibilityConverter" TrueValue="Collapsed" FalseValue="Visible"/> + <converters:EmptyObjectToObjectConverter x:Key="EmptyObjectToObjectConverter" NotEmptyValue="Visible" EmptyValue="Collapsed"/> + </UserControl.Resources> + + <!--- Show the Choose environment header on the page. --> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="auto"/> + <RowDefinition /> + </Grid.RowDefinitions> + + <!--- Make SetupShellContent. --> + <setupControls:SetupShell + Title="{x:Bind ViewModel.PageTitle}" + x:Uid="ConfigureEnvironmentPage" + Orchestrator="{x:Bind ViewModel.Orchestrator, Mode=OneWay}" + Foreground="{ThemeResource TextFillColorSecondary}" + Grid.Row="0" + ContentVisibility="Collapsed"> + </setupControls:SetupShell> + <ScrollViewer + MaxWidth="{ThemeResource MaxPageContentWidth}" + Grid.Row="1"> + <Grid > + <Grid + HorizontalAlignment="Stretch" + Visibility="{x:Bind ViewModel.SessionErrorMessage, Mode=OneWay, Converter={StaticResource EmptyObjectToObjectConverter}}"> + <InfoBar + IsOpen="True" + x:Uid="ErrorRetrievingAdaptiveCardSession" + Severity="Error" + Message="{x:Bind ViewModel.SessionErrorMessage}" > + </InfoBar> + </Grid> + + <!--- Show the adaptive card on the page if its loaded --> + <Grid + Visibility="{x:Bind ViewModel.IsAdaptiveCardSessionLoaded, Mode=OneWay}" + x:Name="AdaptiveCardGrid" /> + + <Grid + HorizontalAlignment="Center" + Visibility="{x:Bind ViewModel.IsAdaptiveCardSessionLoaded, Mode=OneWay, Converter={StaticResource CollapsedWhenTrueBoolToVisibilityConverter}}"> + <ProgressRing + IsActive="True" + Width="25" + Height="25" + Margin="0,150,0,20"/> + </Grid> + </Grid> + </ScrollViewer> + </Grid> +</UserControl> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs new file mode 100644 index 000000000..7a438b6ae --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/EnvironmentCreationOptionsView.xaml.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.SetupFlow.Models.Environments; +using DevHome.SetupFlow.ViewModels.Environments; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Serilog; + +namespace DevHome.SetupFlow.Views.Environments; + +public sealed partial class EnvironmentCreationOptionsView : UserControl, IRecipient<NewAdaptiveCardAvailableMessage> +{ + // Logging to capture any adaptive card rendering exceptions so the app doesn't crash + private readonly ILogger _log = Log.ForContext("SourceContext", nameof(EnvironmentCreationOptionsView)); + + public EnvironmentCreationOptionsViewModel ViewModel => (EnvironmentCreationOptionsViewModel)this.DataContext; + + public EnvironmentCreationOptionsView() + { + this.InitializeComponent(); + WeakReferenceMessenger.Default.Register<NewAdaptiveCardAvailableMessage>(this); + } + + private void ViewUnloaded(object sender, RoutedEventArgs e) + { + AdaptiveCardGrid.Children.Clear(); + WeakReferenceMessenger.Default.UnregisterAll(this); + } + + /// <summary> + /// Request the adaptive cad from the view model + /// </summary> + private void ViewLoaded(object sender, RoutedEventArgs e) + { + var message = WeakReferenceMessenger.Default.Send<CreationOptionsViewPageRequestMessage>(); + if (!message.HasReceivedResponse) + { + return; + } + + AddAdaptiveCardToUI(message.Response); + } + + /// <summary> + /// Receive the adaptive card from the view model, when the view model finishes loading it. + /// Note: There are times when the view is loaded after the view model has finished loading the adaptive card. + /// In these cases it would have "missed" the push message. This is where the ViewLoaded method comes in. + /// </summary> + public void Receive(NewAdaptiveCardAvailableMessage message) + { + // Only process the message if the view model is the EnvironmentCreationOptionsViewModel + if (message.Value.CurrentSetupFlowViewModel is EnvironmentCreationOptionsViewModel) + { + AddAdaptiveCardToUI(message.Value.RenderedAdaptiveCard); + } + } + + private void AddAdaptiveCardToUI(RenderedAdaptiveCard adaptiveCardData) + { + try + { + if (adaptiveCardData?.FrameworkElement != null) + { + AdaptiveCardGrid.Children.Clear(); + AdaptiveCardGrid.Children.Add(adaptiveCardData.FrameworkElement); + } + } + catch (Exception ex) + { + // Log the exception + _log.Error(ex, "Error adding adaptive card UI in EnvironmentCreationOptionsView"); + } + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml new file mode 100644 index 000000000..30efe7f7f --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="utf-8"?> +<UserControl + x:Class="DevHome.SetupFlow.Views.Environments.SelectEnvironmentProviderView" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:setupControls="using:DevHome.SetupFlow.Controls" + xmlns:toolKit="using:CommunityToolkit.WinUI.Controls" + xmlns:EnvironmentViewModels="using:DevHome.SetupFlow.ViewModels.Environments" + xmlns:ic="using:Microsoft.Xaml.Interactions.Core" + xmlns:i="using:Microsoft.Xaml.Interactivity" + xmlns:converters="using:CommunityToolkit.WinUI.Converters"> + <UserControl.Resources> + <ResourceDictionary> + <ResourceDictionary.MergedDictionaries> + <ResourceDictionary Source="ms-appx:///DevHome.Common/Environments/Templates/EnvironmentsTemplates.xaml" /> + </ResourceDictionary.MergedDictionaries> + <converters:BoolToVisibilityConverter x:Key="CollapsedWhenTrueBoolToVisibilityConverter" TrueValue="Collapsed" FalseValue="Visible"/> + + <!-- Template for the compute system providers that we were able to retrieve from all the extensions --> + <DataTemplate x:Key="ProviderItemTemplate" x:DataType="EnvironmentViewModels:ComputeSystemProviderViewModel"> + <ItemContainer + AutomationProperties.Name="{x:Bind DisplayName, Mode=OneWay}" + IsSelected="{x:Bind IsSelected}"> + <toolKit:SettingsCard + HorizontalAlignment="Stretch" + HeaderIcon="{x:Bind Icon}" + Header="{x:Bind DisplayName}"> + </toolKit:SettingsCard> + </ItemContainer> + </DataTemplate> + </ResourceDictionary> + </UserControl.Resources> + + <!--- Show the Select environment header on the page. --> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="auto"/> + <RowDefinition /> + </Grid.RowDefinitions> + <setupControls:SetupShell + Title="{x:Bind ViewModel.PageTitle}" + x:Uid="SelectEnvironmentPage" + Orchestrator="{x:Bind ViewModel.Orchestrator, Mode=OneWay}" + Foreground="{ThemeResource TextFillColorSecondary}" + Grid.Row="0" + ContentVisibility="Collapsed"> + </setupControls:SetupShell> + <ScrollViewer + MaxWidth="{ThemeResource MaxPageContentWidth}" + Grid.Row="1"> + <StackPanel Spacing="10"> + <!--- Show the Select environment subtitle on the page. --> + <TextBlock + x:Uid="SelectEnvironmentSubtitle" + x:Name="LabelForItemsView" + Foreground="{ThemeResource TextFillColorSecondary}"/> + + <!--- List of Compute system providers the user can choose from when they're loaded. --> + <ItemsView + x:Name="ComputeSystemProviderViewModelsList" + AutomationProperties.LabeledBy="{Binding ElementName=LabelForItemsView, Mode=OneWay}" + ItemsSource="{x:Bind ViewModel.ProvidersViewModels, Mode=OneWay}" + ItemTemplate="{StaticResource ProviderItemTemplate}" + Visibility="{x:Bind ViewModel.AreProvidersLoaded, Mode=OneWay}"> + <ItemsView.Layout> + <StackLayout Spacing="5" /> + </ItemsView.Layout> + <i:Interaction.Behaviors> + <ic:EventTriggerBehavior EventName="SelectionChanged"> + <ic:InvokeCommandAction + Command="{Binding ItemsViewSelectionChangedCommand, Mode=OneWay}" + CommandParameter="{Binding SelectedItem, ElementName=ComputeSystemProviderViewModelsList, Mode=OneWay}"/> + </ic:EventTriggerBehavior> + </i:Interaction.Behaviors> + </ItemsView> + + <Grid + HorizontalAlignment="Center" + VerticalAlignment="Center" + Visibility="{x:Bind ViewModel.AreProvidersLoaded, Mode=OneWay, Converter={StaticResource CollapsedWhenTrueBoolToVisibilityConverter}}"> + <ProgressRing + IsActive="True" + Width="25" + Height="25" + Margin="0,150,0,0"/> + </Grid> + </StackPanel> + </ScrollViewer> + </Grid> +</UserControl> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml.cs new file mode 100644 index 000000000..70707a5aa --- /dev/null +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/Environments/SelectEnvironmentProviderView.xaml.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DevHome.SetupFlow.ViewModels.Environments; +using Microsoft.UI.Xaml.Controls; + +namespace DevHome.SetupFlow.Views.Environments; + +public sealed partial class SelectEnvironmentProviderView : UserControl +{ + public SelectEnvironmentProviderViewModel ViewModel => (SelectEnvironmentProviderViewModel)this.DataContext; + + public SelectEnvironmentProviderView() + { + this.InitializeComponent(); + } +} diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml index 36bb2ce58..816896790 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/MainPageView.xaml @@ -164,6 +164,24 @@ <StackPanel Spacing="{StaticResource SettingsCardSpacing}" AutomationProperties.Name="{x:Bind ViewModel.MainPageQuickStepsGroupName}"> <TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" x:Uid="ms-resource:///DevHome.SetupFlow/Resources/MainPage_QuickConfiguration" /> + + <!-- settings card for creating an environment --> + <ctControls:SettingsCard + Visibility="{x:Bind ViewModel.ShouldShowCreateEnvironmentItem}" + x:Uid="MainPageCreateEnvironment" + AutomationProperties.AutomationId="CreateEnvironment" + IsClickEnabled="True" + Command="{x:Bind ViewModel.StartCreateEnvironmentCommand}" + CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Header}" + ActionIconToolTip="{Binding Header, RelativeSource={RelativeSource Mode=Self}}" + AutomationProperties.AccessibilityView="Control" + ActionIcon="{x:Null}" > + <ctControls:SettingsCard.HeaderIcon> + <ImageIcon Source="ms-appx:///DevHome.SetupFlow/Assets/CreateVirtualEnvironment.png" /> + </ctControls:SettingsCard.HeaderIcon> + </ctControls:SettingsCard> + + <!-- settings card for cloning repositories --> <ctControls:SettingsCard x:Uid="ms-resource:///DevHome.SetupFlow/Resources/MainPage_CloneRepos" AutomationProperties.AutomationId="CloneRepoButton" IsClickEnabled="True" diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml index bc2c11c90..a86acf64c 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/RepoConfigView.xaml @@ -146,14 +146,26 @@ VerticalAlignment="Center"/> <!-- Need to use view methods here because DataType is CloningInformation--> <!-- Each button needs a unique name for screen readers. --> - <Button Click="EditClonePathButton_Click" DataContext="{x:Bind}" AutomationProperties.Name="{x:Bind EditClonePathAutomationName}" Grid.Column="1" > + <Button + Click="EditClonePathButton_Click" + DataContext="{x:Bind}" + AutomationProperties.Name="{x:Bind EditClonePathAutomationName}" + Grid.Column="1" + ToolTipService.ToolTip="{x:Bind EditClonePathAutomationName}"> <Button.Content> <SymbolIcon Symbol="Edit"/> </Button.Content> </Button> </Grid> </Border> - <Button Click="RemoveCloningInformationButton_Click" DataContext="{x:Bind}" Grid.Column="3" Style="{ThemeResource AlternateCloseButtonStyle}" AutomationProperties.Name="{x:Bind RemoveFromCloningAutomationName}" VerticalAlignment="Center"> + <Button + Click="RemoveCloningInformationButton_Click" + DataContext="{x:Bind}" + Grid.Column="3" + Style="{ThemeResource AlternateCloseButtonStyle}" + AutomationProperties.Name="{x:Bind RemoveFromCloningAutomationName}" + VerticalAlignment="Center" + ToolTipService.ToolTip="{x:Bind RemoveFromCloningAutomationName}"> <Button.Content> <SymbolIcon Symbol="Cancel"/> </Button.Content> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml index fab007c9e..576ae8c9f 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/ReviewView.xaml @@ -13,6 +13,7 @@ xmlns:controls="using:DevHome.SetupFlow.Controls" xmlns:selectors="using:DevHome.SetupFlow.Selectors" xmlns:viewModels="using:DevHome.SetupFlow.ViewModels" + xmlns:environmentViews="using:DevHome.SetupFlow.Views.Environments" xmlns:views="using:DevHome.SetupFlow.Views" mc:Ignorable="d"> <UserControl.Resources> @@ -51,9 +52,12 @@ </Grid> </setupFlowBehaviors:SetupFlowNavigationBehavior.NextTemplate> - <controls:SetupShell x:Uid="ms-resource:///DevHome.SetupFlow/Resources/SetupShell_Review" - Orchestrator="{x:Bind ViewModel.Orchestrator, Mode=OneWay}" - Foreground="{ThemeResource TextFillColorSecondary}"> + <controls:SetupShell + Title="{x:Bind ViewModel.PageTitle, Mode=OneWay}" + Description="{x:Bind ViewModel.ReviewPageDescription}" + x:Uid="ms-resource:///DevHome.SetupFlow/Resources/SetupShell_Review" + Orchestrator="{x:Bind ViewModel.Orchestrator, Mode=OneWay}" + Foreground="{ThemeResource TextFillColorSecondary}"> <ScrollViewer VerticalScrollBarVisibility="Auto"> <StackPanel> <Expander HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" @@ -67,8 +71,8 @@ </Grid.ColumnDefinitions> <TextBlock VerticalAlignment="Center" - Style="{StaticResource BodyStrongTextBlockStyle}" - x:Uid="Review_SetUpDetails" /> + Style="{StaticResource BodyStrongTextBlockStyle}" + Text="{x:Bind ViewModel.ReviewPageExpanderDescription, Mode=OneWay}" /> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10"> <Button Command="{x:Bind ViewModel.DownloadConfigurationCommand}" x:Uid="Review_GenerateConfigurationFileButton"/> @@ -139,6 +143,11 @@ <views:AppManagementReviewView /> </DataTemplate> </selectors:ReviewTabViewSelector.AppManagementTabTemplate> + <selectors:ReviewTabViewSelector.CreateEnvironmentTabTemplate> + <DataTemplate> + <environmentViews:CreateEnvironmentReviewView /> + </DataTemplate> + </selectors:ReviewTabViewSelector.CreateEnvironmentTabTemplate> </selectors:ReviewTabViewSelector> </ContentControl.ContentTemplateSelector> </ContentControl> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml index 79e5e95a2..c76facd36 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml @@ -9,6 +9,7 @@ xmlns:pg="using:DevHome.Common" xmlns:behaviors="using:DevHome.Common.Behaviors" xmlns:setupFlowBehaviors="using:DevHome.SetupFlow.Behaviors" + xmlns:environmentViews="using:DevHome.SetupFlow.Views.Environments" xmlns:selectors="using:DevHome.SetupFlow.Selectors" xmlns:views="using:DevHome.SetupFlow.Views" xmlns:controls="using:DevHome.SetupFlow.Controls" @@ -73,6 +74,16 @@ <views:ConfigurationFileView/> </DataTemplate> </selectors:SetupFlowViewSelector.ConfigurationFileTemplate> + <selectors:SetupFlowViewSelector.SelectEnvironmentsProviderTemplate> + <DataTemplate> + <environmentViews:SelectEnvironmentProviderView /> + </DataTemplate> + </selectors:SetupFlowViewSelector.SelectEnvironmentsProviderTemplate> + <selectors:SetupFlowViewSelector.EnvironmentCreationOptionsTemplate> + <DataTemplate> + <environmentViews:EnvironmentCreationOptionsView /> + </DataTemplate> + </selectors:SetupFlowViewSelector.EnvironmentCreationOptionsTemplate> </selectors:SetupFlowViewSelector> </ContentControl.ContentTemplateSelector> </ContentControl> diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml.cs index 391add5b2..18b3b778a 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SetupFlowPage.xaml.cs @@ -6,6 +6,7 @@ using DevHome.SetupFlow.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Navigation; namespace DevHome.SetupFlow.Views; @@ -20,4 +21,10 @@ public SetupFlowPage() ViewModel = Application.Current.GetService<SetupFlowViewModel>(); InitializeComponent(); } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + ViewModel.OnNavigatedTo(e); + } } diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml index a245c9c92..ab86d6534 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml @@ -14,6 +14,8 @@ xmlns:controls="using:DevHome.SetupFlow.Controls" setupFlowBehaviors:SetupFlowNavigationBehavior.PreviousVisibility="Collapsed" setupFlowBehaviors:SetupFlowNavigationBehavior.NextVisibility="Collapsed" + Loaded="ViewLoaded" + Unloaded="ViewUnloaded" mc:Ignorable="d"> <UserControl.Resources> <ResourceDictionary> @@ -106,7 +108,7 @@ <TextBlock Style="{ThemeResource SubtitleTextBlockStyle}" Visibility="{x:Bind ViewModel.FailedTasks, Mode=OneWay, Converter={StaticResource NegatedCollectionVisibilityConverter}}" - x:Uid="ms-resource:///DevHome.SetupFlow/Resources/SummaryPage_Header"/> + Text="{x:Bind ViewModel.PageHeaderText}"/> <TextBlock Style="{ThemeResource SubtitleTextBlockStyle}" Visibility="{x:Bind ViewModel.FailedTasks, Mode=OneWay, Converter={StaticResource CollectionVisibilityConverter}}" @@ -265,6 +267,7 @@ <Grid.ColumnDefinitions> <ColumnDefinition Width="{x:Bind ViewModel.AppsDownloaded, Converter={StaticResource EmptyCollectionWillNotSpanColumnsConverter}}"/> <ColumnDefinition Width="{x:Bind ViewModel.RepositoriesCloned, Converter={StaticResource EmptyCollectionWillNotSpanColumnsConverter}}"/> + <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> @@ -309,10 +312,30 @@ Text="{x:Bind ViewModel.RepositoriesClonedText}" Foreground="{ThemeResource TextFillColorSecondary}"/> </Grid> - <Button Grid.Row="1" Grid.ColumnSpan="2" Visibility="{x:Bind ViewModel.FailedTasks, Mode=OneWay, Converter={StaticResource NegatedCollectionVisibilityConverter}}" - x:Uid="ms-resource:///DevHome.SetupFlow/Resources/SummaryPage_OpenDashboard" + <Grid + Grid.Column="2" + Visibility="{x:Bind ViewModel.WasCreateEnvironmentOperationStarted, Mode=OneWay}"> + <Grid.RowDefinitions> + <RowDefinition/> + <RowDefinition/> + </Grid.RowDefinitions> + <!--- There will only ever be 1 environment created in the flow at a time. --> + <TextBlock + Grid.Row="0" + Text="1" + FontSize="{StaticResource TitleLargeTextBlockFontSize}" + FontWeight="{StaticResource InfoBarTitleFontWeight}" + HorizontalAlignment="Center"/> + <TextBlock + Grid.Row="1" + HorizontalAlignment="Center" + x:Uid="SummaryPageEnvironmentCreating" + Foreground="{ThemeResource TextFillColorSecondary}"/> + </Grid> + <Button Grid.Row="1" Grid.ColumnSpan="3" Visibility="{x:Bind ViewModel.FailedTasks, Mode=OneWay, Converter={StaticResource NegatedCollectionVisibilityConverter}}" + Content="{x:Bind ViewModel.PageRedirectButtonText}" HorizontalAlignment="Center" - Command="{x:Bind ViewModel.GoToDashboardCommand, Mode=OneWay}" + Command="{x:Bind ViewModel.RedirectToNextPageCommand, Mode=OneWay}" Style="{StaticResource AccentButtonStyle}" Margin="0, 50, 0, 0"/> </Grid> @@ -374,6 +397,18 @@ </Expander> </Grid> + <!-- Create environment details expander --> + <Grid Padding="0,13" Visibility="{x:Bind ViewModel.WasCreateEnvironmentOperationStarted}" + BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" + BorderThickness="{ThemeResource TopNavigationViewContentGridBorderThickness}"> + <Expander> + <Expander.Header> + <TextBlock x:Uid="SummaryPageHeadingForCreateEnvironmentFlow" Style="{ThemeResource BodyStrongTextBlockStyle}"/> + </Expander.Header> + <Grid x:Name="AdaptiveCardGrid" /> + </Expander> + </Grid> + <!-- Apps downloaded --> <Grid Padding="0,12" diff --git a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml.cs b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml.cs index c89feb8d8..4ade95a2b 100644 --- a/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml.cs +++ b/tools/SetupFlow/DevHome.SetupFlow/Views/SummaryView.xaml.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using AdaptiveCards.Rendering.WinUI3; +using CommunityToolkit.Mvvm.Messaging; +using DevHome.SetupFlow.Models.Environments; using DevHome.SetupFlow.ViewModels; using DevHome.SetupFlow.Windows; using Microsoft.UI.Xaml; @@ -8,11 +11,12 @@ namespace DevHome.SetupFlow.Views; -public sealed partial class SummaryView : UserControl -{ +public sealed partial class SummaryView : UserControl, IRecipient<NewAdaptiveCardAvailableMessage> +{ public SummaryView() { this.InitializeComponent(); + WeakReferenceMessenger.Default.Register<NewAdaptiveCardAvailableMessage>(this); } public SummaryViewModel ViewModel => (SummaryViewModel)this.DataContext; @@ -36,4 +40,51 @@ private void InstallationNotes_IsTextTrimmedChanged(TextBlock sender, IsTextTrim viewAllButton.Visibility = sender.IsTextTrimmed ? Visibility.Visible : Visibility.Collapsed; } } + + private void ViewUnloaded(object sender, RoutedEventArgs e) + { + AdaptiveCardGrid.Children.Clear(); + WeakReferenceMessenger.Default.UnregisterAll(this); + } + + /// <summary> + /// Receive the adaptive card from the view model, when the view model finishes loading it. + /// Note: There are times when the view is loaded after the view model has finished loading the adaptive card. + /// In these cases it would have "missed" the push message. This is where the ViewLoaded method comes in. + /// </summary> + public void Receive(NewAdaptiveCardAvailableMessage message) + { + // Only process the message if the view model is the SummaryViewModel + if (message.Value.CurrentSetupFlowViewModel is SummaryViewModel) + { + AddAdaptiveCardToUI(message.Value.RenderedAdaptiveCard); + } + } + + /// <summary> + /// Request the adaptive cad from the EnvironmentCreationOptionsViewModel object when we're in the environment + /// creation flow. + /// </summary> + private void ViewLoaded(object sender, RoutedEventArgs e) + { + var message = WeakReferenceMessenger.Default.Send<CreationOptionsReviewPageDataRequestMessage>(); + if (!message.HasReceivedResponse) + { + return; + } + + AddAdaptiveCardToUI(message.Response); + } + + private void AddAdaptiveCardToUI(RenderedAdaptiveCard renderedAdaptiveCard) + { + var frameworkElement = renderedAdaptiveCard?.FrameworkElement; + if (frameworkElement == null) + { + return; + } + + AdaptiveCardGrid.Children.Clear(); + AdaptiveCardGrid.Children.Add(frameworkElement); + } } diff --git a/uitest/DevHome.UITest.csproj b/uitest/DevHome.UITest.csproj index 9cf0059ba..c480aa09c 100644 --- a/uitest/DevHome.UITest.csproj +++ b/uitest/DevHome.UITest.csproj @@ -3,7 +3,7 @@ <PropertyGroup> <RootNamespace>DevHome.UITest</RootNamespace> <Platforms>x86;x64;arm64</Platforms> - <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers> + <RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <IsPackable>false</IsPackable> <ImplicitUsings>enable</ImplicitUsings> <UseWinUI>true</UseWinUI>