Skip to content

Commit

Permalink
Finish UI for environments creation (#2539)
Browse files Browse the repository at this point in the history
* add initial changes

* add parsers

* fix build

* update existing classes and xaml

* add hyper-v adaptive card for creation

* add changes for hyper-v

* fix build

* add changes to show settings cards from hyper v

* update with latest changes

* add recent changes

* fix build

* add updates for adaptive cards to appear in review page

* add creation to experiements page and fix adaptive card not showing up after moving to a different page

* add missing message classes

* Delete tools/SetupFlow/DevHome.SetupFlow/Models/Environments/CreateEnvironmentTask.cs

* update message passing

* add initial changes

* add more updates

* update based on comments, and add more comments

* update comments and itemsviews choice set to prevent the choiceset from holding onto items view control internally

* Use adaptive card render service to render adaptive card within content dialog

* add updates

* add

* update json

* add more updates

* add more changes

* only update UI if status method returns a mail

* add updates for UI

* add finishing UI changed

* update based on comments and make sure a new create environment flow is made when you click the create environment button

* update spelling
  • Loading branch information
bbonaby authored Apr 9, 2024
1 parent efdffbc commit cfd0cb0
Show file tree
Hide file tree
Showing 57 changed files with 1,485 additions and 227 deletions.
15 changes: 12 additions & 3 deletions HyperVExtension/src/HyperVExtension/Extensions/StreamExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HyperVExtension.Models;

namespace HyperVExtension.Extensions;

Expand All @@ -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)
{
Expand All @@ -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;
}
}
}
}
28 changes: 28 additions & 0 deletions HyperVExtension/src/HyperVExtension/Models/ByteTransferProgress.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public IAsyncOperation<ProviderOperationResult> OnAction(string action, string i
switch (_creationAdaptiveCard?.State)
{
case "initialCreationForm":
operationResult = await HandleActionWhenFormInInitalState(actionPayload, inputs);
operationResult = await HandleActionWhenFormInInitialState(actionPayload, inputs);
break;
case "reviewForm":
(operationResult, shouldEndSession) = await HandleActionWhenFormInReviewState(actionPayload);
Expand Down Expand Up @@ -158,16 +158,16 @@ private ProviderOperationResult GetInitialCreationFormAdaptiveCard()
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 },
};
{
{ "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);
}
Expand Down Expand Up @@ -211,7 +211,7 @@ private async Task<ProviderOperationResult> GetForReviewFormAdaptiveCardAsync(st
}

var galleryImage = _vMGalleryImageList.Images[inputForGalleryOperation.SelectedImageListIndex];
var newVirtualMachineNameLabel = _stringResource.GetLocalized("NameLabelForNewVirtualMachine", ":");
var newEnvironmentNameLabel = _stringResource.GetLocalized("NameLabelForNewVirtualMachine", ":");
var primaryButtonForCreationFlowText = _stringResource.GetLocalized("PrimaryButtonLabelForCreationFlow");
var secondaryButtonForCreationFlowText = _stringResource.GetLocalized("SecondaryButtonLabelForCreationFlow");
var storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(Constants.ExtensionIconInternal));
Expand All @@ -227,8 +227,8 @@ private async Task<ProviderOperationResult> GetForReviewFormAdaptiveCardAsync(st
{ "DiskImageSize", BytesHelper.ConvertBytesToString(galleryImage.Disk.SizeInBytes) },
{ "VMGalleryImageName", galleryImage.Name },
{ "Publisher", galleryImage.Publisher },
{ "NameOfNewVM", inputForGalleryOperation.NewVirtualMachineName },
{ "NameLabel", newVirtualMachineNameLabel },
{ "NameOfNewVM", inputForGalleryOperation.NewEnvironmentName },
{ "NameLabel", newEnvironmentNameLabel },
{ "Base64ImageForProvider", providerBase64Image },
{ "DiskImageUrl", galleryImage.Symbol.Uri },
{ "PrimaryButtonLabelForCreationFlow", primaryButtonForCreationFlowText },
Expand All @@ -247,7 +247,7 @@ private async Task<ProviderOperationResult> GetForReviewFormAdaptiveCardAsync(st
}

/// <summary>
/// The discription for VM gallery images is stored in a list of strings. This method merges the strings into one string.
/// 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>
Expand Down Expand Up @@ -298,7 +298,7 @@ private JsonObject SetupContentDialogInfo(VMGalleryImage image)
};
}

private async Task<ProviderOperationResult> HandleActionWhenFormInInitalState(AdaptiveCardActionPayload actionPayload, string inputs)
private async Task<ProviderOperationResult> HandleActionWhenFormInInitialState(AdaptiveCardActionPayload actionPayload, string inputs)
{
ProviderOperationResult operationResult;
var actionButtonId = actionPayload.Id ?? string.Empty;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;

namespace HyperVExtension.Models.VirtualMachineCreation;

/// <summary>
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,5 @@ public interface IOperationReport

public string LocalizationKey { get; }

public ulong BytesReceived { get; }

public ulong TotalBytesToReceive { get; }
public ByteTransferProgress ProgressObject { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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)
{
Expand All @@ -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,
Expand All @@ -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("Failed to update progress", ex);
}
}

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("Failed to update progress", ex);
}
}

/// <summary>
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using HyperVExtension.Extensions;
using HyperVExtension.Models;
using HyperVExtension.Models.VirtualMachineCreation;

namespace HyperVExtension.Services;
Expand Down Expand Up @@ -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"/>
Expand Down
12 changes: 8 additions & 4 deletions HyperVExtension/src/HyperVExtension/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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">
Expand Down Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"body": [
{
"type": "Input.Text",
"id": "NewVirtualMachineName",
"id": "NewEnvironmentName",
"label": "${EnterNewVMNameLabel}",
"placeholder": "${EnterNewVMNamePlaceHolder}",
"maxLength": 100,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
});

Expand Down
Loading

0 comments on commit cfd0cb0

Please sign in to comment.