diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/CreateDevicePageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/CreateDevicePageTests.cs index 25ca76177..da881c6a2 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/CreateDevicePageTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/CreateDevicePageTests.cs @@ -51,6 +51,8 @@ public override void Setup() _ = Services.AddSingleton(this.mockDeviceClientService.Object); _ = Services.AddSingleton(this.mockLoRaWanDeviceClientService.Object); + _ = Services.AddSingleton(); + Services.Add(new ServiceDescriptor(typeof(IResizeObserver), new MockResizeObserver())); this.mockNavigationManager = Services.GetRequiredService(); diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceDetailPageTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceDetailPageTests.cs index 41798576f..eb36eb282 100644 --- a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceDetailPageTests.cs +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceDetailPageTests.cs @@ -55,6 +55,8 @@ public override void Setup() _ = Services.AddSingleton(this.mockDeviceClientService.Object); _ = Services.AddSingleton(this.mockLoRaWanDeviceClientService.Object); + _ = Services.AddSingleton(); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = false }); Services.Add(new ServiceDescriptor(typeof(IResizeObserver), new MockResizeObserver())); diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceToDuplicateSelectorTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceToDuplicateSelectorTests.cs new file mode 100644 index 000000000..1b3a311ac --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Pages/Devices/DeviceToDuplicateSelectorTests.cs @@ -0,0 +1,110 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.Pages.Devices +{ + using System.Collections.Generic; + using AngleSharp.Dom; + using AutoFixture; + using AzureIoTHub.Portal.Client.Services; + using Bunit; + using Client.Exceptions; + using Client.Models; + using Client.Pages.Devices; + using FluentAssertions; + using Microsoft.Extensions.DependencyInjection; + using Models.v10; + using Moq; + using MudBlazor; + using NUnit.Framework; + + [TestFixture] + public class DeviceToDuplicateSelectorTests : BlazorUnitTest + { + private Mock mockDeviceClientService; + private Mock mockLoRaWanDeviceClientService; + private Mock mockDeviceModelsClientService; + private Mock mockLoRaWanDeviceModelsClientService; + + public override void Setup() + { + base.Setup(); + + this.mockDeviceClientService = MockRepository.Create(); + this.mockLoRaWanDeviceClientService = MockRepository.Create(); + this.mockDeviceModelsClientService = MockRepository.Create(); + this.mockLoRaWanDeviceModelsClientService = MockRepository.Create(); + + _ = Services.AddSingleton(this.mockDeviceClientService.Object); + _ = Services.AddSingleton(this.mockLoRaWanDeviceClientService.Object); + _ = Services.AddSingleton(this.mockDeviceModelsClientService.Object); + _ = Services.AddSingleton(this.mockLoRaWanDeviceModelsClientService.Object); + + _ = Services.AddSingleton(); + } + + [Test] + public void DeviceToDuplicateSelectorShouldRenderCorrectly() + { + // Act + var cut = RenderComponent(); + + // Assert + cut.WaitForAssertion(() => cut.FindAll("#search-device").Count.Should().Be(1)); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public void TypingOnMudAutocompleteShouldTriggerSearch() + { + // Arrange + var query = Fixture.Create(); + + var url = $"api/devices?pageSize=10&searchText={query}"; + _ = this.mockDeviceClientService.Setup(service => service.GetDevices(url)) + .ReturnsAsync(new PaginationResult() + { + Items = new List + { + new() + { + DeviceID = Fixture.Create() + } + } + }); + + var cut = RenderComponent(); + var autocompleteComponent = cut.FindComponent>(); + + // Act + autocompleteComponent.Find(TagNames.Input).Click(); + autocompleteComponent.Find(TagNames.Input).Input(query); + + // Assert + cut.WaitForAssertion(() => autocompleteComponent.Instance.IsOpen.Should().BeTrue()); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + + [Test] + public void TypingOnMudAutocompleteShouldProcessProblemDetailsExceptionWhenTriggerSearch() + { + // Arrange + var query = Fixture.Create(); + + var url = $"api/devices?pageSize=10&searchText={query}"; + _ = this.mockDeviceClientService.Setup(service => service.GetDevices(url)) + .ThrowsAsync(new ProblemDetailsException(new ProblemDetailsWithExceptionDetails())); + + var cut = RenderComponent(); + var autocompleteComponent = cut.FindComponent>(); + + // Act + autocompleteComponent.Find(TagNames.Input).Click(); + autocompleteComponent.Find(TagNames.Input).Input(query); + + // Assert + cut.WaitForAssertion(() => autocompleteComponent.Instance.IsOpen.Should().BeTrue()); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server.Tests.Unit/Services/DeviceLayoutServiceTests.cs b/src/AzureIoTHub.Portal.Server.Tests.Unit/Services/DeviceLayoutServiceTests.cs new file mode 100644 index 000000000..b70e96114 --- /dev/null +++ b/src/AzureIoTHub.Portal.Server.Tests.Unit/Services/DeviceLayoutServiceTests.cs @@ -0,0 +1,176 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Server.Tests.Unit.Services +{ + using System.Collections.Generic; + using System.Linq; + using AutoFixture; + using Client.Services; + using FluentAssertions; + using Microsoft.Extensions.DependencyInjection; + using Models.v10; + using Models.v10.LoRaWAN; + using NUnit.Framework; + + [TestFixture] + public class DeviceLayoutServiceTests : BlazorUnitTest + { + private IDeviceLayoutService deviceLayoutService; + + public override void Setup() + { + base.Setup(); + + _ = Services.AddSingleton(); + + this.deviceLayoutService = Services.GetRequiredService(); + } + + [Test] + public void RefreshDeviceShouldRaiseRefreshDeviceOccurredEvent() + { + // Arrange + var receivedEvents = new List(); + this.deviceLayoutService.RefreshDeviceOccurred += (sender, _) => + { + receivedEvents.Add(sender?.GetType().ToString()); + }; + + // Act + this.deviceLayoutService.RefreshDevice(); + + // Assert + _ = receivedEvents.Count.Should().Be(1); + _ = receivedEvents.First().Should().Be(typeof(DeviceLayoutService).ToString()); + } + + [Test] + public void DuplicateSharedDeviceShouldReturnDuplicatedDevice() + { + // Arrange + var deviceId = Fixture.Create(); + var deviceName = Fixture.Create(); + + // Act + var result = this.deviceLayoutService.DuplicateSharedDevice(new DeviceDetails + { + DeviceID = deviceId, + DeviceName = deviceName + }); + + // Assert + _ = result.DeviceID.Should().BeEmpty(); + _ = result.DeviceName.Should().Be($"{deviceName} - copy"); + } + + [Test] + public void DuplicateSharedDeviceShouldReturnDuplicatedLoraWanDevice() + { + // Arrange + var deviceId = Fixture.Create(); + var deviceName = Fixture.Create(); + var appKey = Fixture.Create(); + + // Act + var result = this.deviceLayoutService.DuplicateSharedDevice(new LoRaDeviceDetails + { + DeviceID = deviceId, + DeviceName = deviceName, + AppKey = appKey + }); + + // Assert + var loraWanDevice = (LoRaDeviceDetails) result; + + _ = loraWanDevice.DeviceID.Should().BeEmpty(); + _ = loraWanDevice.DeviceName.Should().Be($"{deviceName} - copy"); + _ = loraWanDevice.AppKey.Should().BeEmpty(); + } + + [Test] + public void DuplicateSharedDeviceModelShouldReturnDuplicatedDeviceModel() + { + // Arrange + var deviceModel = Fixture.Create(); + + // Act + var result = this.deviceLayoutService.DuplicateSharedDeviceModel(deviceModel); + + // Assert + _ = result.Should().BeEquivalentTo(deviceModel); + } + + [Test] + public void ResetSharedDeviceShouldReturnNewDevice() + { + // Arrange + var expectedDevice = new DeviceDetails(); + + // Act + var result = this.deviceLayoutService.ResetSharedDevice(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedDevice); + } + + [Test] + public void ResetSharedDeviceShouldReturnNewDeviceWithExpectedTags() + { + // Arrange + var expectedTags = Fixture.CreateMany(2).ToList(); + + var expectedDevice = new DeviceDetails(); + + foreach (var tag in expectedTags) + { + _ = expectedDevice.Tags.TryAdd(tag.Name, string.Empty); + } + + // Act + var result = this.deviceLayoutService.ResetSharedDevice(expectedTags); + + // Assert + _ = result.Should().BeEquivalentTo(expectedDevice); + } + + [Test] + public void ResetSharedDeviceModelShouldReturnNewDeviceModel() + { + // Arrange + var expectedDeviceModel = new DeviceModel(); + + // Act + var result = this.deviceLayoutService.ResetSharedDeviceModel(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedDeviceModel); + } + + [Test] + public void GetSharedDeviceShouldReturnDevice() + { + // Arrange + var expectedDevice = new DeviceDetails(); + + // Act + var result = this.deviceLayoutService.GetSharedDevice(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedDevice); + } + + [Test] + public void GetSharedDeviceModelShouldReturnDeviceModel() + { + // Arrange + var expectedDeviceModel = new DeviceModel(); + + // Act + var result = this.deviceLayoutService.GetSharedDeviceModel(); + + // Assert + _ = result.Should().BeEquivalentTo(expectedDeviceModel); + } + } +} diff --git a/src/AzureIoTHub.Portal/Client/Enums/DeviceSaveAction.cs b/src/AzureIoTHub.Portal/Client/Enums/DeviceSaveAction.cs new file mode 100644 index 000000000..56a5f7d5e --- /dev/null +++ b/src/AzureIoTHub.Portal/Client/Enums/DeviceSaveAction.cs @@ -0,0 +1,13 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Client.Enums +{ + public enum DeviceSaveAction + { + Save, + SaveAndAddNew, + SaveAndDuplicate, + Duplicate + } +} diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor index f29e1f5df..2e1273d0e 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/CreateDevicePage.razor @@ -5,6 +5,7 @@ @using AzureIoTHub.Portal.Models @using AzureIoTHub.Portal.Models.v10 @using AzureIoTHub.Portal.Models.v10.LoRaWAN +@using Microsoft.AspNetCore.Components @attribute [Authorize] @inject ISnackbar Snackbar @@ -14,6 +15,9 @@ @inject IDeviceTagSettingsClientService DeviceTagSettingsClientService @inject IDeviceClientService DeviceClientService @inject ILoRaWanDeviceClientService LoRaWanDeviceClientService +@inject IDeviceLayoutService DeviceLayoutService + +@implements IDisposable Create Device @@ -32,7 +36,14 @@ - Save Changes + + @saveButtonText + + Save + Save and add new + Save and duplicate + + @@ -46,55 +57,65 @@ - this.DeviceModel) - Variant="Variant.Outlined" - ToStringFunc="@(x => x?.Name)" - ResetValueOnEmptyText=true - Immediate=true - Clearable=true - CoerceText=true - CoerceValue=false> - - @context.Name - - @((!string.IsNullOrEmpty(@context.Description) && @context.Description.Length > 100) ? @context.Description.Substring(0, 100) + "..." : @context.Description) - - - - @if (Device.ModelId == null && displayValidationErrorMessages) + @if (duplicateDevice) { -

The Model is required.

+ + } + else + { + this.DeviceModel) + Variant="Variant.Outlined" + ToStringFunc="@(x => x?.Name)" + ResetValueOnEmptyText=true + Immediate=true + Clearable=true + CoerceText=true + CoerceValue=false> + + @context.Name + + @((!string.IsNullOrEmpty(@context.Description) && @context.Description.Length > 100) ? @context.Description.Substring(0, 100) + "..." : @context.Description) + + + + @if (Device.ModelId == null && displayValidationErrorMessages) + { +

The Model is required.

+ } }
@if (IsLoRa) { + id=@nameof(DeviceDetails.DeviceID) + Label="Device ID / DevEUI" + Variant="Variant.Outlined" + Validation=@(standardValidator.ValidateValue) + For="@(()=> Device.DeviceID)" + Mask="@maskLoRaDeviceID" + HelperText="DeviceID must contain 16 hexadecimal characters (numbers from 0 to 9 and/or letters from A to F)" /> } else { + id=@nameof(DeviceDetails.DeviceID) + Label="Device ID" + Variant="Variant.Outlined" + Validation=@(standardValidator.ValidateValue) + For="@(()=> Device.DeviceID)" + HelperText="The device identifier should be of ASCII 7-bit alphanumeric characters plus certain special characters" /> } + + + DeviceModelList.SingleOrDefault(c => c.ModelId == Device.ModelId)?.SupportLoRaFeatures ?? false; + private bool IsLoRa => DeviceModel?.SupportLoRaFeatures ?? false; public PatternMask maskLoRaDeviceID = new PatternMask("XXXXXXXXXXXXXXXX") { @@ -242,15 +265,13 @@ private static char AllUpperCase(char c) => c.ToString().ToUpperInvariant()[0]; - private DeviceDetails Device { get; set; } = new DeviceDetails(); - private IEnumerable DeviceModelList { get; set; } = new List(); private DeviceModel _deviceModel; private DeviceModel DeviceModel { - get { return _deviceModel; } - set { ChangeModel(value).Wait(); } + get => _deviceModel; + set { Task.Run(async () => await ChangeModel(value)); } } private IEnumerable TagList { get; set; } = new List(); @@ -259,11 +280,19 @@ private bool displayValidationErrorMessages = false; private bool isProcessing; + private bool duplicateDevice; + + private DeviceSaveAction deviceSaveAction = DeviceSaveAction.Save; + private string saveButtonText = "Save"; protected override async Task OnInitializedAsync() { try { + DeviceLayoutService.RefreshDeviceOccurred += DeviceServiceOnRefreshDeviceOccurred; + Device = DeviceLayoutService.GetSharedDevice(); + DeviceModel = DeviceLayoutService.GetSharedDeviceModel(); + // Enable device by default Device.IsEnabled = true; @@ -273,9 +302,9 @@ // Gets the custom tags that can be set when creating a device TagList = await DeviceTagSettingsClientService.GetDeviceTags(); - foreach (DeviceTag tag in TagList) + foreach (var tag in TagList) { - Device.Tags.Add(tag.Name, ""); + Device.Tags.TryAdd(tag.Name, string.Empty); } } catch (ProblemDetailsException exception) @@ -284,6 +313,13 @@ } } + public void Dispose() + { + DeviceLayoutService.ResetSharedDevice(); + DeviceLayoutService.ResetSharedDeviceModel(); + DeviceLayoutService.RefreshDeviceOccurred -= DeviceServiceOnRefreshDeviceOccurred; + } + /// /// Sends a POST request to the DevicesController, to add the new device to the Azure IoT Hub /// @@ -321,8 +357,7 @@ // Prompts a snack bar to inform the action was successful Snackbar.Add($"Device {Device.DeviceID} has been successfully created!", Severity.Success); - // Go back to the list of devices - NavManager.NavigateTo("devices"); + ProcessPostDeviceCreation(); } catch (ProblemDetailsException exception) { @@ -330,10 +365,35 @@ } finally { - isProcessing = false; + isProcessing = false; + } + } + + private void ProcessPostDeviceCreation() + { + switch (deviceSaveAction) + { + case DeviceSaveAction.Save: + NavManager.NavigateTo("devices"); + break; + case DeviceSaveAction.SaveAndAddNew: + Device = DeviceLayoutService.ResetSharedDevice(TagList.ToList()); + DeviceModel = DeviceLayoutService.ResetSharedDeviceModel(); + break; + case DeviceSaveAction.SaveAndDuplicate: + Device = DeviceLayoutService.DuplicateSharedDevice(Device); + DeviceModel = DeviceLayoutService.DuplicateSharedDeviceModel(DeviceModel); + break; } } + private void DeviceServiceOnRefreshDeviceOccurred(object sender, EventArgs e) + { + Device = DeviceLayoutService.GetSharedDevice(); + DeviceModel = DeviceLayoutService.GetSharedDeviceModel(); + StateHasChanged(); + } + private bool CheckTagsError() { bool tagValidationError = false; @@ -366,6 +426,20 @@ .Where(x => x.Name.StartsWith(value, StringComparison.InvariantCultureIgnoreCase)); } + private void SetSaveButtonText(DeviceSaveAction saveAction) + { + deviceSaveAction = saveAction; + saveButtonText = deviceSaveAction switch + { + DeviceSaveAction.Save => "Save", + DeviceSaveAction.SaveAndAddNew => "Save and add new", + DeviceSaveAction.SaveAndDuplicate => "Save and duplicate", + _ => saveButtonText + }; + } + + + internal async Task ChangeModel(DeviceModel model) { try @@ -384,7 +458,7 @@ Tags = Device.Tags }; - if (model == null) + if (model == null || string.IsNullOrWhiteSpace(model.ModelId)) { return; } @@ -393,15 +467,15 @@ { var device = new LoRaDeviceDetails { - DeviceID = Device.DeviceID, + DeviceID = this.Device.DeviceID, ModelId = model.ModelId, ImageUrl = model.ImageUrl, - DeviceName = Device.DeviceName, - IsEnabled = Device.IsEnabled, - Tags = Device.Tags + DeviceName = this.Device.DeviceName, + IsEnabled = this.Device.IsEnabled, + Tags = this.Device.Tags }; - Device = device; + this.Device = device; var loRaDeviceModelResult = await LoRaWanDeviceModelsClientService.GetDeviceModel(model.ModelId); @@ -412,21 +486,25 @@ else { var properties = await DeviceModelsClientService.GetDeviceModelModelProperties(model.ModelId); - + Properties.AddRange(properties.Select(x => new DevicePropertyValue - { - DisplayName = x.DisplayName, - IsWritable = x.IsWritable, - Name = x.Name, - Order = x.Order, - PropertyType = x.PropertyType - })); + { + DisplayName = x.DisplayName, + IsWritable = x.IsWritable, + Name = x.Name, + Order = x.Order, + PropertyType = x.PropertyType + })); } } catch (ProblemDetailsException exception) { Error?.ProcessProblemDetails(exception); } + finally + { + await InvokeAsync(StateHasChanged); + } } } diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor index 4eb82c0aa..8fac40d08 100644 --- a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceDetailPage.razor @@ -15,6 +15,7 @@ @inject IDeviceTagSettingsClientService DeviceTagSettingsClientService @inject IDeviceClientService DeviceClientService @inject ILoRaWanDeviceClientService LoRaWanDeviceClientService +@inject IDeviceLayoutService DeviceLayoutService @@ -47,8 +48,14 @@ - Delete device - Save Changes + Delete device + + @saveButtonText + + Save + Duplicate + + @@ -230,6 +237,9 @@ private bool isProcessing; + private DeviceSaveAction deviceSaveAction = DeviceSaveAction.Save; + private string saveButtonText = "Save"; + private void Return() => navigationManager.NavigateTo("devices"); private IEnumerable Commands { get; set; } @@ -268,6 +278,21 @@ } } + private void ProcessActionOnDevice() + { + switch (deviceSaveAction) + { + case DeviceSaveAction.Save: + Save(); + break; + case DeviceSaveAction.Duplicate: + DeviceLayoutService.DuplicateSharedDevice(Device); + DeviceLayoutService.DuplicateSharedDeviceModel(DeviceModel); + NavManager.NavigateTo("devices/new"); + break; + } + } + /// /// Sends a POST request to the DevicesController, to add the new device to the Azure IoT Hub /// @@ -305,7 +330,6 @@ // Prompts a snack bar to inform the action was successful Snackbar.Add($"Device {Device.DeviceName} has been successfully updated!", Severity.Success, null); - // Go back to the list of devices NavManager.NavigateTo("devices"); } catch (ProblemDetailsException exception) @@ -314,9 +338,11 @@ } finally { - isProcessing = false; + isProcessing = false; + StateHasChanged(); } } + public async Task ShowConnectionString() { var parameters = new DialogParameters(); @@ -365,4 +391,15 @@ // Go back to the list of devices NavManager.NavigateTo("devices"); } + + private void SetSaveButtonText(DeviceSaveAction saveAction) + { + deviceSaveAction = saveAction; + saveButtonText = deviceSaveAction switch + { + DeviceSaveAction.Save => "Save", + DeviceSaveAction.Duplicate => "Duplicate", + _ => saveButtonText + }; + } } diff --git a/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceToDuplicateSelector.razor b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceToDuplicateSelector.razor new file mode 100644 index 000000000..4ff950a4d --- /dev/null +++ b/src/AzureIoTHub.Portal/Client/Pages/Devices/DeviceToDuplicateSelector.razor @@ -0,0 +1,89 @@ +@using AzureIoTHub.Portal.Models.v10 +@using Microsoft.AspNetCore.Components +@using System.Web + +@inject IDeviceClientService DeviceClientService +@inject ILoRaWanDeviceClientService LoRaWanDeviceClientService +@inject IDeviceModelsClientService DeviceModelsClientService +@inject ILoRaWanDeviceModelsClientService LoRaWanDeviceModelsClientService +@inject IDeviceLayoutService DeviceLayoutService + + + + @context.DeviceName + + Id: @context.DeviceID + + + LoRaWAN: @(@context.SupportLoRaFeatures ? "Yes" : "No") + + + + + Type to search devices by id or name + + + + +@code { + [CascadingParameter] + public Error Error {get; set;} + + private async Task> SearchDevicesToDuplicate(string query) + { + try + { + if (string.IsNullOrWhiteSpace(query)) + { + return Array.Empty(); + } + var uri = $"api/devices?pageSize=10&searchText={HttpUtility.UrlEncode(query)}"; + var result = await DeviceClientService.GetDevices(uri); + return result.Items; + } + catch (ProblemDetailsException exception) + { + Error?.ProcessProblemDetails(exception); + return new List(); + } + } + + private async Task OnDeviceSelected(DeviceListItem deviceToDuplicate) + { + try + { + DeviceDetails device; + DeviceModel deviceModel; + + if (deviceToDuplicate.SupportLoRaFeatures) + { + device = await LoRaWanDeviceClientService.GetDevice(deviceToDuplicate.DeviceID); + deviceModel = await LoRaWanDeviceModelsClientService.GetDeviceModel(device.ModelId); + } + else + { + device = await DeviceClientService.GetDevice(deviceToDuplicate.DeviceID); + deviceModel = await DeviceModelsClientService.GetDeviceModel(device.ModelId); + } + DeviceLayoutService.DuplicateSharedDevice(device); + DeviceLayoutService.DuplicateSharedDeviceModel(deviceModel); + DeviceLayoutService.RefreshDevice(); + } + catch (ProblemDetailsException exception) + { + Error?.ProcessProblemDetails(exception); + } + } +} diff --git a/src/AzureIoTHub.Portal/Client/Program.cs b/src/AzureIoTHub.Portal/Client/Program.cs index 8d68fa44f..71562049a 100644 --- a/src/AzureIoTHub.Portal/Client/Program.cs +++ b/src/AzureIoTHub.Portal/Client/Program.cs @@ -54,6 +54,7 @@ public static async Task Main(string[] args) _ = builder.Services.AddScoped(); _ = builder.Services.AddScoped(); + _ = builder.Services.AddSingleton(); _ = builder.Services.AddScoped(); _ = builder.Services.AddScoped(); diff --git a/src/AzureIoTHub.Portal/Client/Services/DeviceLayoutService.cs b/src/AzureIoTHub.Portal/Client/Services/DeviceLayoutService.cs new file mode 100644 index 000000000..698e79d62 --- /dev/null +++ b/src/AzureIoTHub.Portal/Client/Services/DeviceLayoutService.cs @@ -0,0 +1,81 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Client.Services +{ + using System; + using System.Collections.Generic; + using Portal.Models.v10; + using Portal.Models.v10.LoRaWAN; + + public class DeviceLayoutService : IDeviceLayoutService + { + private DeviceDetails sharedDevice = new(); + private DeviceModel sharedDeviceModel = new(); + + public event EventHandler RefreshDeviceOccurred; + + public void RefreshDevice() + { + OnRefreshDeviceOccurred(); + } + + public DeviceDetails GetSharedDevice() + { + return this.sharedDevice; + } + + public DeviceModel GetSharedDeviceModel() + { + return this.sharedDeviceModel; + } + + public DeviceDetails ResetSharedDevice(List tags = null) + { + this.sharedDevice = new DeviceDetails(); + + foreach (var tag in tags ?? new List()) + { + _ = this.sharedDevice.Tags.TryAdd(tag.Name, string.Empty); + } + + return this.sharedDevice; + } + + public DeviceModel ResetSharedDeviceModel() + { + this.sharedDeviceModel = new DeviceModel(); + + return this.sharedDeviceModel; + } + + public DeviceDetails DuplicateSharedDevice(DeviceDetails deviceToDuplicate) + { + deviceToDuplicate.DeviceID = string.Empty; + deviceToDuplicate.DeviceName = $"{deviceToDuplicate.DeviceName} - copy"; + + if (deviceToDuplicate.IsLoraWan) + { + var loRaDeviceDetails = (LoRaDeviceDetails)deviceToDuplicate; + loRaDeviceDetails.AppKey = string.Empty; + + this.sharedDevice = loRaDeviceDetails; + } + else + { + this.sharedDevice = deviceToDuplicate; + } + + return this.sharedDevice; + } + + public DeviceModel DuplicateSharedDeviceModel(DeviceModel deviceModelToDuplicate) + { + this.sharedDeviceModel = deviceModelToDuplicate; + + return this.sharedDeviceModel; + } + + private void OnRefreshDeviceOccurred() => RefreshDeviceOccurred?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/AzureIoTHub.Portal/Client/Services/IDeviceLayoutService.cs b/src/AzureIoTHub.Portal/Client/Services/IDeviceLayoutService.cs new file mode 100644 index 000000000..313ed5219 --- /dev/null +++ b/src/AzureIoTHub.Portal/Client/Services/IDeviceLayoutService.cs @@ -0,0 +1,23 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Client.Services +{ + using System; + using System.Collections.Generic; + using Portal.Models.v10; + + public interface IDeviceLayoutService + { + event EventHandler RefreshDeviceOccurred; + + void RefreshDevice(); + + DeviceDetails GetSharedDevice(); + DeviceModel GetSharedDeviceModel(); + DeviceDetails ResetSharedDevice(List tags = null); + DeviceModel ResetSharedDeviceModel(); + DeviceDetails DuplicateSharedDevice(DeviceDetails deviceToDuplicate); + DeviceModel DuplicateSharedDeviceModel(DeviceModel deviceModelToDuplicate); + } +} diff --git a/src/AzureIoTHub.Portal/Client/_Imports.razor b/src/AzureIoTHub.Portal/Client/_Imports.razor index 06e231f6e..bb045d72c 100644 --- a/src/AzureIoTHub.Portal/Client/_Imports.razor +++ b/src/AzureIoTHub.Portal/Client/_Imports.razor @@ -16,4 +16,5 @@ @using AzureIoTHub.Portal.Client.Extensions @using AzureIoTHub.Portal.Client.Services @using AzureIoTHub.Portal.Client.Models +@using AzureIoTHub.Portal.Client.Enums @using MudBlazor