using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Remotely.Server.Enums; using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; using Remotely.Shared.Attributes; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Threading.Tasks; namespace Remotely.Server.Components.Devices { [Authorize] public partial class DevicesFrame : AuthComponentBase, IDisposable { private readonly List _allDevices = new(); private readonly string _deviceGroupAll = Guid.NewGuid().ToString(); private readonly string _deviceGroupNone = Guid.NewGuid().ToString(); private readonly List _deviceGroups = new(); private readonly List _devicesForPage = new(); private readonly object _devicesLock = new(); private readonly List _filteredDevices = new(); private readonly List _sortableProperties = new(); private int _currentPage = 1; private int _devicesPerPage = 25; private string _filter; private bool _hideOfflineDevices = true; private string _selectedGroupId; private string _selectedSortProperty = "DeviceName"; private ListSortDirection _sortDirection; [Inject] private IClientAppState AppState { get; set; } [Inject] private ICircuitConnection CircuitConnection { get; set; } [Inject] private IDataService DataService { get; set; } [Inject] private IJsInterop JsInterop { get; set; } [Inject] private ILogger Logger { get; set; } [Inject] private IToastService ToastService { get; set; } private int TotalPages => (int)Math.Max(1, Math.Ceiling((decimal)_filteredDevices.Count / _devicesPerPage)); public void Dispose() { CircuitConnection.MessageReceived -= CircuitConnection_MessageReceived; AppState.PropertyChanged -= AppState_PropertyChanged; GC.SuppressFinalize(this); } public void Refresh() { LoadDevices(); InvokeAsync(StateHasChanged); } protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; AppState.PropertyChanged += AppState_PropertyChanged; _deviceGroups.Clear(); _deviceGroups.AddRange(DataService.GetDeviceGroups(User.UserName)); _selectedGroupId = _deviceGroupAll; var sortableProperties = typeof(Device) .GetProperties() .Where(x => x.CustomAttributes.Any(x => x.AttributeType == typeof(SortableAttribute))); _sortableProperties.AddRange(sortableProperties); LoadDevices(); } protected override bool ShouldRender() { var shouldRender = base.ShouldRender(); if (shouldRender) { FilterDevices(); } return shouldRender; } private void AddScriptResult(ScriptResult result) { var device = DataService.GetDevice(result.DeviceID); AppState.AddTerminalLine($"{device?.DeviceName} @ {result.TimeStamp}", "font-weight-bold"); foreach (var line in result.StandardOutput) { AppState.AddTerminalLine(line, "text-info"); } foreach (var line in result.ErrorOutput) { AppState.AddTerminalLine(line, "text-danger"); } AppState.InvokePropertyChanged(nameof(AppState.TerminalLines)); } private void AppState_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(AppState.DevicesFrameFocusedCardState) || e.PropertyName == nameof(AppState.DevicesFrameFocusedDevice) || e.PropertyName == nameof(AppState.DevicesFrameSelectedDevices)) { InvokeAsync(StateHasChanged); } } private void CircuitConnection_MessageReceived(object sender, CircuitEvent args) { switch (args.EventName) { case CircuitEventName.DeviceUpdate: case CircuitEventName.DeviceWentOffline: { if (args.Params?.FirstOrDefault() is Device device) { lock (_devicesLock) { var index = _allDevices.FindIndex(x => x.ID == device.ID); if (index > -1) { _allDevices[index] = device; } index = _devicesForPage.FindIndex(x => x.ID == device.ID); if (index > -1) { _devicesForPage[index] = device; } } Debouncer.Debounce(TimeSpan.FromSeconds(2), Refresh); } break; } case CircuitEventName.DisplayMessage: { var terminalMessage = (string)args.Params[0]; var toastMessage = (string)args.Params[1]; var className = (string)args.Params[2]; AppState.AddTerminalLine(terminalMessage); ToastService.ShowToast(toastMessage, classString: className); InvokeAsync(StateHasChanged); } break; case CircuitEventName.ScriptResult: { var result = (ScriptResult)args.Params[0]; AddScriptResult(result); } break; default: break; } } private void ClearSelectedCard() { AppState.DevicesFrameFocusedDevice = null; AppState.DevicesFrameFocusedCardState = DeviceCardState.Normal; } private void FilterDevices() { lock (_devicesLock) { _filteredDevices.Clear(); var appendDevices = new List(); foreach (var device in _allDevices) { if (AppState.DevicesFrameSelectedDevices.Contains(device.ID)) { appendDevices.Add(device); } if (!device.IsOnline && _hideOfflineDevices) { continue; } if (!string.IsNullOrWhiteSpace(_filter) && device.Alias?.Contains(_filter, StringComparison.OrdinalIgnoreCase) != true && device.CurrentUser?.Contains(_filter, StringComparison.OrdinalIgnoreCase) != true && device.DeviceName?.Contains(_filter, StringComparison.OrdinalIgnoreCase) != true && device.Notes?.Contains(_filter, StringComparison.OrdinalIgnoreCase) != true && device.Platform?.Contains(_filter, StringComparison.OrdinalIgnoreCase) != true && device.Tags?.Contains(_filter, StringComparison.OrdinalIgnoreCase) != true) { continue; } if (_selectedGroupId == _deviceGroupAll || _selectedGroupId == device.DeviceGroupID || ( _selectedGroupId == _deviceGroupNone && string.IsNullOrWhiteSpace(device.DeviceGroupID ))) { _filteredDevices.Add(device); } } if (!string.IsNullOrWhiteSpace(_selectedSortProperty)) { var direction = _sortDirection == ListSortDirection.Ascending ? 1 : -1; _filteredDevices.Sort((a, b) => { if (a.IsOnline != b.IsOnline) { return b.IsOnline.CompareTo(a.IsOnline); } var propInfo = _sortableProperties.Find(x => x.Name == _selectedSortProperty); var valueA = propInfo.GetValue(a); var valueB = propInfo.GetValue(b); return Comparer.Default.Compare(valueA, valueB) * direction; }); } var skipCount = (_currentPage - 1) * _devicesPerPage; var devicesForPage = _filteredDevices .Except(appendDevices) .Skip(skipCount) .Take(_devicesPerPage); _devicesForPage.Clear(); _devicesForPage.AddRange(appendDevices.Concat(devicesForPage)); } } private string GetDisplayName(PropertyInfo propInfo) { return propInfo.GetCustomAttribute()?.Name ?? propInfo.Name; } private string GetSortIcon() { return $"oi-sort-{_sortDirection.ToString().ToLower()}"; } private void HandleRefreshClicked() { Refresh(); ToastService.ShowToast("Devices refreshed."); } private void LoadDevices() { lock (_devicesLock) { _allDevices.Clear(); var devices = DataService.GetDevicesForUser(Username) .OrderByDescending(x => x.IsOnline) .ToList(); _allDevices.AddRange(devices); } FilterDevices(); } private void PageDown() { if (_currentPage > 1) { _currentPage--; } } private void PageUp() { if (_currentPage < TotalPages) { _currentPage++; } } private void SelectAllCards() { if (AppState.DevicesFrameSelectedDevices.Any()) { AppState.DevicesFrameSelectedDevices.Clear(); } else { foreach (var device in _filteredDevices) { if (!AppState.DevicesFrameSelectedDevices.Contains(device.ID)) { AppState.DevicesFrameSelectedDevices.Add(device.ID); } }; } AppState.InvokePropertyChanged(nameof(AppState.DevicesFrameSelectedDevices)); } private void ToggleSortDirection() { if (_sortDirection == ListSortDirection.Ascending) { _sortDirection = ListSortDirection.Descending; } else { _sortDirection = ListSortDirection.Ascending; } } } }