using Immense.RemoteControl.Server.Abstractions; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Remotely.Server.Auth; using Remotely.Server.Enums; using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using Remotely.Shared.ViewModels; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; namespace Remotely.Server.Components.Devices { public partial class DeviceCard : AuthComponentBase, IDisposable { private readonly ConcurrentDictionary _fileUploadProgressLookup = new(); private ElementReference _card; private Theme _theme; private Version _currentVersion = new(); [Parameter] public Device Device { get; set; } [CascadingParameter] public DevicesFrame ParentFrame { get; set; } [Inject] private IClientAppState AppState { get; set; } [Inject] private ICircuitConnection CircuitConnection { get; set; } [Inject] private IServiceHubSessionCache ServiceSessionCache { get; init; } [Inject] private IUpgradeService UpgradeService { get; init; } [Inject] private IDataService DataService { get; set; } private bool IsExpanded => GetCardState() == DeviceCardState.Expanded; private bool IsOutdated => Version.TryParse(Device.AgentVersion, out var result) && result < _currentVersion; private bool IsSelected => AppState.DevicesFrameSelectedDevices.Contains(Device.ID); [Inject] private IJsInterop JsInterop { get; set; } [Inject] private IModalService ModalService { get; set; } [Inject] private IToastService ToastService { get; set; } public void Dispose() { AppState.PropertyChanged -= AppState_PropertyChanged; CircuitConnection.MessageReceived -= CircuitConnection_MessageReceived; GC.SuppressFinalize(this); } protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); _theme = await AppState.GetEffectiveTheme(); _currentVersion = UpgradeService.GetCurrentVersion(); AppState.PropertyChanged += AppState_PropertyChanged; CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; } private void AppState_PropertyChanged(object sender, System.ComponentModel.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 e) { switch (e.EventName) { case CircuitEventName.DeviceUpdate: case CircuitEventName.DeviceWentOffline: { if (e.Params?.FirstOrDefault() is Device device && device.ID == Device?.ID) { Device = device; InvokeAsync(StateHasChanged); } break; } default: break; } } private void ContextMenuOpening(MouseEventArgs args) { if (GetCardState() == DeviceCardState.Normal) { JsInterop.OpenWindow($"/device-details/{Device.ID}", "_blank"); } } private async Task ExpandCard(MouseEventArgs args) { if (AppState.DevicesFrameFocusedDevice == Device.ID) { if (AppState.DevicesFrameFocusedCardState == DeviceCardState.Normal) { AppState.DevicesFrameFocusedCardState = DeviceCardState.Expanded; } return; } AppState.DevicesFrameFocusedDevice = Device.ID; AppState.DevicesFrameFocusedCardState = DeviceCardState.Expanded; JsInterop.ScrollToElement(_card); await CircuitConnection.TriggerHeartbeat(Device.ID); } private DeviceCardState GetCardState() { if (AppState.DevicesFrameFocusedDevice == Device.ID) { return AppState.DevicesFrameFocusedCardState; } return DeviceCardState.Normal; } private string GetCardStateClass(Device device) { if (AppState.DevicesFrameFocusedDevice == device.ID) { return AppState.DevicesFrameFocusedCardState.ToString().ToLower(); } return string.Empty; } private string GetProgressMessage(string key) { if (_fileUploadProgressLookup.TryGetValue(key, out var value)) { return $"{MathHelper.GetFormattedPercent(value)} - {key}"; } return string.Empty; } private void HandleHeaderClick() { if (IsExpanded) { SetCardStateNormal(); } } private async Task HandleValidSubmit() { if (!DataService.DoesUserHaveAccessToDevice(Device.ID, User)) { ToastService.ShowToast("Unauthorized.", classString: "bg-warning"); return; } DataService.UpdateDevice(Device.ID, Device.Tags, Device.Alias, Device.DeviceGroupID, Device.Notes); ToastService.ShowToast("Device settings saved."); await CircuitConnection.TriggerHeartbeat(Device.ID); } private async Task OnFileInputChanged(InputFileChangeEventArgs args) { ToastService.ShowToast("File upload started."); var fileId = await DataService.AddSharedFile(args.File, User.OrganizationID, OnFileInputProgress); var transferId = Guid.NewGuid().ToString(); var result = await CircuitConnection.TransferFileFromBrowserToAgent(Device.ID, transferId, new[] { fileId }); if (!result) { ToastService.ShowToast("Device not found.", classString: "bg-warning"); } else { ToastService.ShowToast("File upload completed."); } } private void OnFileInputProgress(double percentComplete, string fileName) { if (_fileUploadProgressLookup.TryGetValue(fileName, out var existingValue) && percentComplete < 1 && percentComplete - existingValue < .05) { // Avoid too frequent of updates. return; } _fileUploadProgressLookup.AddOrUpdate(fileName, percentComplete, (k, v) => percentComplete); InvokeAsync(StateHasChanged); } private void OpenDeviceDetails() { JsInterop.OpenWindow($"/device-details/{Device.ID}", "_blank"); } private void SetCardStateNormal() { AppState.DevicesFrameFocusedDevice = null; AppState.DevicesFrameFocusedCardState = DeviceCardState.Normal; } private void ShowAllDisks() { var disksString = JsonSerializer.Serialize(Device.Drives, JsonSerializerHelper.IndentedOptions); void modalBody(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { builder.AddMarkupContent(0, $"
{disksString}
"); } ModalService.ShowModal($"All Disks for {Device.DeviceName}", modalBody); } private void StartChat() { var existingSession = AppState.DevicesFrameChatSessions.FirstOrDefault(x => x.DeviceId == Device.ID); if (existingSession is null) { AppState.DevicesFrameChatSessions.Add(new ChatSession() { DeviceId = Device.ID, DeviceName = Device.DeviceName, IsExpanded = true }); } else { existingSession.IsExpanded = true; } AppState.InvokePropertyChanged(nameof(AppState.DevicesFrameChatSessions)); } private async Task StartRemoteControl(bool viewOnly) { if (!ServiceSessionCache.TryGetConnectionId(Device.ID, out _)) { ToastService.ShowToast("Device connection not found", classString: "bg-danger"); return; } var result = await CircuitConnection.RemoteControl(Device.ID, viewOnly); if (!result.IsSuccess) { return; } var session = result.Value; if (!await session.WaitForSessionReady(TimeSpan.FromSeconds(20))) { ToastService.ShowToast("Session failed to start", classString: "bg-danger"); return; } JsInterop.OpenWindow( $"/RemoteControl/Viewer" + $"?mode=Unattended&sessionId={session.UnattendedSessionId}" + $"&accessKey={session.AccessKey}" + $"&viewonly={viewOnly}", "_blank"); } private void ToggleIsSelected(ChangeEventArgs args) { var isSelected = (bool)args.Value; if (isSelected) { AppState.DevicesFrameSelectedDevices.Add(Device.ID); } else { AppState.DevicesFrameSelectedDevices.Remove(Device.ID); } AppState.InvokePropertyChanged(nameof(AppState.DevicesFrameSelectedDevices)); } private async Task UninstallAgent() { var result = await JsInterop.Confirm("Are you sure you want to uninstall this agent? This is permanent!"); if (result) { await CircuitConnection.UninstallAgents(new[] { Device.ID }); AppState.DevicesFrameFocusedDevice = null; AppState.DevicesFrameFocusedCardState = DeviceCardState.Normal; ParentFrame.Refresh(); } } } }