diff --git a/src/AzureIoTHub.Portal.Client/Pages/Devices/DeviceListPage.razor b/src/AzureIoTHub.Portal.Client/Pages/Devices/DeviceListPage.razor index 5469c4967..579577109 100644 --- a/src/AzureIoTHub.Portal.Client/Pages/Devices/DeviceListPage.razor +++ b/src/AzureIoTHub.Portal.Client/Pages/Devices/DeviceListPage.razor @@ -9,13 +9,14 @@ @inject PortalSettings Portal @inject IDeviceTagSettingsClientService DeviceTagSettingsClientService @inject IDeviceClientService DeviceClientService +@inject IDeviceModelsClientService DeviceModelsClientService - + @foreach (DeviceTagDto tag in TagList) @@ -27,6 +28,29 @@ } } + + this.Model) + 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) + + + + Status @@ -168,11 +192,24 @@ private string searchID = ""; private string searchStatus; private string searchState; + private MudTable table; private Dictionary searchTags = new(); private bool IsLoading { get; set; } = true; + private List ModelList = new List(); + private DeviceModelDto _model; + + public DeviceModelDto Model + { + get => _model; + set + { + Task.Run(async () => await ChangeModel(value)); + } + } + private IEnumerable TagList { get; set; } = new List(); private int[] pageSizeOptions = new int[] { 2, 5, 10 }; @@ -181,6 +218,7 @@ { try { + this.ModelList = (await DeviceModelsClientService.GetDeviceModels()).ToList(); // Gets the custom tags that can be searched via the panel TagList = await DeviceTagSettingsClientService.GetDeviceTags(); foreach (var tag in TagList) @@ -212,7 +250,7 @@ break; } - var uri = $"api/devices?pageNumber={state.Page}&pageSize={state.PageSize}&searchText={HttpUtility.UrlEncode(searchID)}&searchStatus={searchStatus}&searchState={searchState}&orderBy={orderBy}"; + var uri = $"api/devices?pageNumber={state.Page}&pageSize={state.PageSize}&searchText={HttpUtility.UrlEncode(searchID)}&searchStatus={searchStatus}&searchState={searchState}&orderBy={orderBy}&modelId={this.Model?.ModelId}"; foreach (var searchTag in searchTags.Where(c => !string.IsNullOrEmpty(c.Value))) { @@ -292,4 +330,44 @@ { navigationManager.NavigateTo($"devices/{item.DeviceID}{((item.SupportLoRaFeatures && Portal.IsLoRaSupported) ? "?isLora=true" : "")}"); } + + internal async Task ChangeModel(DeviceModelDto Model) + { + try + { + this._model = Model; + + if (Model == null || string.IsNullOrWhiteSpace(Model.ModelId)) + { + return; + } + + } + catch (ProblemDetailsException exception) + { + Error?.ProcessProblemDetails(exception); + } + finally + { + await InvokeAsync(StateHasChanged); + } + } + + /// + /// Allows to autocomplete the Device Model field in the form. + /// + /// Text entered in the field + /// Item of the device model list that matches the user's value + public async Task> Search(string value) + { + // In real life use an asynchronous function for fetching data from an api. + await Task.Delay(0); + + // if text is null or empty, show complete list + if (string.IsNullOrEmpty(value)) + return ModelList; + + return ModelList + .Where(x => x.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase)); + } } diff --git a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/DevicesController.cs b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/DevicesController.cs index 76be98f8f..0a728ad76 100644 --- a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/DevicesController.cs +++ b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/DevicesController.cs @@ -36,6 +36,9 @@ public DevicesController( /// /// /// + /// + /// + /// [HttpGet(Name = "GET Device list")] public Task> SearchItems( string searchText = null, @@ -43,9 +46,10 @@ public Task> SearchItems( bool? searchState = null, int pageSize = 10, int pageNumber = 0, - [FromQuery] string[] orderBy = null) + [FromQuery] string[] orderBy = null, + string modelId = null) { - return GetItems("GET Device list", searchText, searchStatus, searchState, pageSize, pageNumber, orderBy); + return GetItems("GET Device list", searchText, searchStatus, searchState, pageSize, pageNumber, orderBy, modelId); } /// diff --git a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/DevicesControllerBase.cs b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/DevicesControllerBase.cs index 96024ae62..fe4ea5b9e 100644 --- a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/DevicesControllerBase.cs +++ b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/DevicesControllerBase.cs @@ -41,6 +41,7 @@ protected DevicesControllerBase( /// /// /// + /// protected async Task> GetItems( string routeName = null, string searchText = null, @@ -48,7 +49,8 @@ protected async Task> GetItems( bool? searchState = null, int pageSize = 10, int pageNumber = 0, - string[] orderBy = null) + string[] orderBy = null, + string modelId = null) { var paginatedDevices = await this.deviceService.GetDevices( @@ -58,7 +60,8 @@ protected async Task> GetItems( pageSize, pageNumber, orderBy, - GetTagsFromQueryString(Request.Query)); + GetTagsFromQueryString(Request.Query), + modelId); var nextPage = string.Empty; diff --git a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/LoRaWAN/LoRaWANDevicesController.cs b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/LoRaWAN/LoRaWANDevicesController.cs index af267658f..c3d702c20 100644 --- a/src/AzureIoTHub.Portal.Server/Controllers/v1.0/LoRaWAN/LoRaWANDevicesController.cs +++ b/src/AzureIoTHub.Portal.Server/Controllers/v1.0/LoRaWAN/LoRaWANDevicesController.cs @@ -45,6 +45,7 @@ public LoRaWANDevicesController( /// /// /// + /// [HttpGet(Name = "GET LoRaWAN device list")] public Task> SearchItems( string searchText = null, @@ -52,9 +53,10 @@ public Task> SearchItems( bool? searchState = null, int pageSize = 10, int pageNumber = 0, - [FromQuery] string[] orderBy = null) + [FromQuery] string[] orderBy = null, + string modelId = null) { - return GetItems("GET LoRaWAN device list", searchText, searchStatus, searchState, pageSize, pageNumber, orderBy); + return GetItems("GET LoRaWAN device list", searchText, searchStatus, searchState, pageSize, pageNumber, orderBy, modelId); } /// diff --git a/src/AzureIoTHub.Portal.Server/Services/DeviceServiceBase.cs b/src/AzureIoTHub.Portal.Server/Services/DeviceServiceBase.cs index 7ee4c5890..c1e69cc13 100644 --- a/src/AzureIoTHub.Portal.Server/Services/DeviceServiceBase.cs +++ b/src/AzureIoTHub.Portal.Server/Services/DeviceServiceBase.cs @@ -43,7 +43,7 @@ protected DeviceServiceBase(PortalDbContext portalDbContext, } public async Task> GetDevices(string searchText = null, bool? searchStatus = null, bool? searchState = null, int pageSize = 10, - int pageNumber = 0, string[] orderBy = null, Dictionary tags = default) + int pageNumber = 0, string[] orderBy = null, Dictionary tags = default, string modelId = null) { var deviceListFilter = new DeviceListFilter { @@ -53,7 +53,8 @@ public async Task> GetDevices(string searchText IsEnabled = searchStatus, Keyword = searchText, OrderBy = orderBy, - Tags = GetSearchableTags(tags) + Tags = GetSearchableTags(tags), + ModelId = modelId }; var devicePredicate = PredicateBuilder.True(); @@ -86,6 +87,12 @@ public async Task> GetDevices(string searchText value.Name.Equals(keyValuePair.Key) && value.Value.Equals(keyValuePair.Value))); } + if (!string.IsNullOrWhiteSpace(deviceListFilter.ModelId)) + { + devicePredicate = devicePredicate.And(device => device.DeviceModelId.Equals(deviceListFilter.ModelId)); + lorawanDevicePredicate = lorawanDevicePredicate.And(device => device.DeviceModelId.Equals(deviceListFilter.ModelId)); + } + var query = this.portalDbContext.Devices .Include(device => device.Tags) .Where(devicePredicate) diff --git a/src/AzureIoTHub.Portal.Server/Services/IDeviceService.cs b/src/AzureIoTHub.Portal.Server/Services/IDeviceService.cs index edccdb3bd..b95eeb88a 100644 --- a/src/AzureIoTHub.Portal.Server/Services/IDeviceService.cs +++ b/src/AzureIoTHub.Portal.Server/Services/IDeviceService.cs @@ -19,7 +19,8 @@ Task> GetDevices( int pageSize = 10, int pageNumber = 0, string[] orderBy = null, - Dictionary tags = default); + Dictionary tags = default, + string modelId = null); Task GetDevice(string deviceId); diff --git a/src/AzureIoTHub.Portal.Shared/Models/v1.0/Filters/DeviceListFilter.cs b/src/AzureIoTHub.Portal.Shared/Models/v1.0/Filters/DeviceListFilter.cs index 7aa0840c8..f04f5c79d 100644 --- a/src/AzureIoTHub.Portal.Shared/Models/v1.0/Filters/DeviceListFilter.cs +++ b/src/AzureIoTHub.Portal.Shared/Models/v1.0/Filters/DeviceListFilter.cs @@ -14,5 +14,7 @@ public class DeviceListFilter : PaginationFilter public bool? IsConnected { get; set; } public Dictionary Tags { get; set; } + + public string ModelId { get; set; } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Devices/DevicesListPageTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Devices/DevicesListPageTests.cs index 2b4ff31ac..d1bc0b228 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Devices/DevicesListPageTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Client/Pages/Devices/DevicesListPageTests.cs @@ -20,6 +20,7 @@ namespace AzureIoTHub.Portal.Tests.Unit.Client.Pages.Devices using Moq; using MudBlazor; using NUnit.Framework; + using System.Linq; [TestFixture] public class DevicesListPageTests : BlazorUnitTest @@ -27,6 +28,7 @@ public class DevicesListPageTests : BlazorUnitTest private Mock mockDialogService; private Mock mockDeviceTagSettingsClientService; private Mock mockDeviceClientService; + private Mock mockDeviceModelsClientService; private readonly string apiBaseUrl = "api/devices"; @@ -37,10 +39,13 @@ public override void Setup() this.mockDialogService = MockRepository.Create(); this.mockDeviceTagSettingsClientService = MockRepository.Create(); this.mockDeviceClientService = MockRepository.Create(); + this.mockDeviceModelsClientService = MockRepository.Create(); _ = Services.AddSingleton(this.mockDialogService.Object); _ = Services.AddSingleton(this.mockDeviceTagSettingsClientService.Object); _ = Services.AddSingleton(this.mockDeviceClientService.Object); + _ = Services.AddSingleton(this.mockDeviceModelsClientService.Object); + _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); } [Test] @@ -52,8 +57,11 @@ public void DeviceListPageRendersCorrectly() _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) .ReturnsAsync(new List()); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List()); + _ = this.mockDeviceClientService.Setup(service => - service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=")) + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) .ReturnsAsync(new PaginationResult { Items = Array.Empty() @@ -76,7 +84,7 @@ public async Task WhenResetFilterButtonClickShouldClearFilters() { // Arrange _ = this.mockDeviceClientService.Setup(service => - service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=")) + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) .ReturnsAsync(new PaginationResult { Items = Array.Empty() @@ -87,6 +95,9 @@ public async Task WhenResetFilterButtonClickShouldClearFilters() _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) .ReturnsAsync(new List()); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List()); + // Act var cut = RenderComponent(); @@ -113,11 +124,13 @@ public void WhenAddNewDeviceClickShouldNavigateToNewDevicePage() { // Arrange _ = this.mockDeviceClientService.Setup(service => - service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=")) + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) .ReturnsAsync(new PaginationResult { Items = Array.Empty() }); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List()); _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); @@ -142,7 +155,7 @@ public void ClickOnRefreshShouldReloadDevices() { // Arrange _ = this.mockDeviceClientService.Setup(service => - service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=")) + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) .ReturnsAsync(new PaginationResult { Items = Array.Empty() @@ -153,6 +166,9 @@ public void ClickOnRefreshShouldReloadDevices() _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) .ReturnsAsync(new List()); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List()); + var cut = RenderComponent(); // Act @@ -169,7 +185,7 @@ public void WhenLoraFeatureDisableClickToItemShouldRedirectToDeviceDetailsPage() var deviceId = Guid.NewGuid().ToString(); _ = this.mockDeviceClientService.Setup(service => - service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=")) + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) .ReturnsAsync(new PaginationResult { Items = new[] { new DeviceListItem { DeviceID = deviceId } } @@ -180,6 +196,9 @@ public void WhenLoraFeatureDisableClickToItemShouldRedirectToDeviceDetailsPage() _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) .ReturnsAsync(new List()); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List()); + var cut = RenderComponent(); cut.WaitForAssertion(() => cut.Markup.Should().NotContain("Loading...")); @@ -197,7 +216,7 @@ public void WhenLoraFeatureEnableClickToItemShouldRedirectToLoRaDeviceDetailsPag var deviceId = Guid.NewGuid().ToString(); _ = this.mockDeviceClientService.Setup(service => - service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=")) + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) .ReturnsAsync(new PaginationResult { Items = new[] { new DeviceListItem { DeviceID = deviceId, SupportLoRaFeatures = true } } @@ -208,6 +227,9 @@ public void WhenLoraFeatureEnableClickToItemShouldRedirectToLoRaDeviceDetailsPag _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) .ReturnsAsync(new List()); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List()); + var cut = RenderComponent(); cut.WaitForAssertion(() => cut.Markup.Should().NotContain("Loading...")); @@ -223,7 +245,7 @@ public void OnInitializedAsyncShouldProcessProblemDetailsExceptionWhenIssueOccur { // Arrange _ = this.mockDeviceClientService.Setup(service => - service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=")) + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) .ReturnsAsync(new PaginationResult { Items = Array.Empty() @@ -234,6 +256,9 @@ public void OnInitializedAsyncShouldProcessProblemDetailsExceptionWhenIssueOccur _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) .ThrowsAsync(new ProblemDetailsException(new ProblemDetailsWithExceptionDetails())); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List()); + // Act var cut = RenderComponent(); @@ -249,13 +274,15 @@ public void LoadItemsShouldProcessProblemDetailsExceptionWhenIssueOccursOnGettin { // Arrange _ = this.mockDeviceClientService.Setup(service => - service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=")) + service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) .ThrowsAsync(new ProblemDetailsException(new ProblemDetailsWithExceptionDetails())); _ = Services.AddSingleton(new PortalSettings { IsLoRaSupported = true }); _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) .ReturnsAsync(new List()); + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List()); // Act var cut = RenderComponent(); @@ -266,5 +293,62 @@ public void LoadItemsShouldProcessProblemDetailsExceptionWhenIssueOccursOnGettin cut.WaitForAssertion(() => MockRepository.VerifyAll()); } + + [Test] + public async Task FliterBySelectModelShould() + { + // Arrange + //var expectedUrl = "api/devices?pageNumber=0&pageSize=10&searchText=&searchStatus=&orderBy=&modelId="; + _ = this.mockDeviceTagSettingsClientService.Setup(service => service.GetDeviceTags()) + .ReturnsAsync(new List()); + + _ = this.mockDeviceClientService.Setup(service => service.GetDevices($"{this.apiBaseUrl}?pageNumber=0&pageSize=10&searchText=&searchStatus=&searchState=&orderBy=&modelId=")) + .ReturnsAsync(new PaginationResult + { + Items = new List + { + new DeviceListItem() + { + DeviceID = Guid.NewGuid().ToString(), + DeviceName = Guid.NewGuid().ToString(), + }, + new DeviceListItem() + { + DeviceID = Guid.NewGuid().ToString(), + DeviceName = Guid.NewGuid().ToString(), + } + } + }); + + _ = this.mockDeviceModelsClientService.Setup(service => service.GetDeviceModels()) + .ReturnsAsync(new List() + { + new DeviceModelDto() + { + Name = "model_01", + Description = Guid.NewGuid().ToString(), + }, + new DeviceModelDto() + { + Name = "model_02", + Description = Guid.NewGuid().ToString(), + }, + }); + + // Act + var popoverProvider = RenderComponent(); + var cut = RenderComponent(); + + cut.WaitForElement($"#{nameof(DeviceModelDto.ModelId)}").Click(); + + popoverProvider.WaitForAssertion(() => popoverProvider.FindAll(".mud-input-helper-text").Count.Should().Be(2)); + + var newModelList = await cut.Instance.Search("01"); + + // Assert + cut.WaitForAssertion(() => cut.Markup.Should().NotContain("Loading...")); + Assert.AreEqual(1, newModelList.Count()); + cut.WaitForAssertion(() => MockRepository.VerifyAll()); + } } } diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/DevicesControllerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/DevicesControllerTests.cs index efe98b365..cb687ef22 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/DevicesControllerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/DevicesControllerTests.cs @@ -99,7 +99,7 @@ public async Task GetListStateUnderTestExpectedBehavior() _ = this.mockDeviceService.Setup(service => service.GetDevices(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny>())) + It.IsAny>(), It.IsAny())) .ReturnsAsync(expectedPaginatedDevices); var locationUrl = "http://location/devices"; diff --git a/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/LoRaWAN/LoRaWANDevicesControllerTests.cs b/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/LoRaWAN/LoRaWANDevicesControllerTests.cs index 7ce0ad900..b34719d86 100644 --- a/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/LoRaWAN/LoRaWANDevicesControllerTests.cs +++ b/src/AzureIoTHub.Portal.Tests.Unit/Server/Controllers/v1.0/LoRaWAN/LoRaWANDevicesControllerTests.cs @@ -115,7 +115,7 @@ public async Task GetListStateUnderTestExpectedBehavior() _ = this.mockDeviceService.Setup(service => service.GetDevices(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny>())) + It.IsAny>(), It.IsAny())) .ReturnsAsync(expectedPaginatedDevices); var locationUrl = "http://location/devices";