mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
388 lines
12 KiB
C#
388 lines
12 KiB
C#
using Bitbound.SimpleMessenger;
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Forms;
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
using Remotely.Server.Enums;
|
|
using Remotely.Server.Hubs;
|
|
using Remotely.Server.Models.Messages;
|
|
using Remotely.Server.Services;
|
|
using Remotely.Server.Services.Stores;
|
|
using Remotely.Shared;
|
|
using Remotely.Shared.Entities;
|
|
using Remotely.Shared.Enums;
|
|
using Remotely.Shared.Utilities;
|
|
using Remotely.Shared.ViewModels;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Remotely.Server.Components.Devices;
|
|
|
|
public partial class DeviceCard : AuthComponentBase
|
|
{
|
|
private readonly ConcurrentDictionary<string, double> _fileUploadProgressLookup = new();
|
|
private ElementReference _card;
|
|
private Version _currentVersion = new();
|
|
private Theme _theme;
|
|
private DeviceCardState _state;
|
|
private DeviceGroup[] _deviceGroups = Array.Empty<DeviceGroup>();
|
|
|
|
[Parameter]
|
|
public Device Device { get; set; } = null!;
|
|
|
|
[CascadingParameter]
|
|
public DevicesFrame ParentFrame { get; init; } = null!;
|
|
|
|
|
|
[Inject]
|
|
public required ISelectedCardsStore SelectedCards { get; init; }
|
|
|
|
[Inject]
|
|
public required IThemeProvider ThemeProvider { get; init; }
|
|
|
|
[Inject]
|
|
public required ICircuitConnection CircuitConnection { get; init; }
|
|
|
|
[Inject]
|
|
public required IDataService DataService { get; init; }
|
|
|
|
[Inject]
|
|
public required IChatSessionStore ChatCache { get; init; }
|
|
|
|
[Inject]
|
|
public required ILogger<DeviceCard> Logger { get; init; }
|
|
|
|
private bool IsExpanded => _state == DeviceCardState.Expanded;
|
|
|
|
private bool IsOutdated =>
|
|
Version.TryParse(Device.AgentVersion, out var result) &&
|
|
result < _currentVersion;
|
|
|
|
private bool IsSelected => SelectedCards.SelectedDevices.Contains(Device.ID);
|
|
|
|
[Inject]
|
|
private IJsInterop JsInterop { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private IModalService ModalService { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private IAgentHubSessionCache ServiceSessionCache { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private IToastService ToastService { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private IUpgradeService UpgradeService { get; init; } = null!;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await base.OnInitializedAsync();
|
|
EnsureUserSet();
|
|
_theme = await ThemeProvider.GetEffectiveTheme();
|
|
_currentVersion = UpgradeService.GetCurrentVersion();
|
|
_deviceGroups = DataService.GetDeviceGroups(UserName);
|
|
|
|
await Register<DeviceCardStateChangedMessage, string>(
|
|
CircuitConnection.ConnectionId,
|
|
HandleDeviceCardStateChanged);
|
|
|
|
await Register<DeviceStateChangedMessage, string>(
|
|
CircuitConnection.ConnectionId,
|
|
HandleDeviceStateChanged);
|
|
}
|
|
|
|
private async Task HandleDeviceCardStateChanged(object subscriber, DeviceCardStateChangedMessage message)
|
|
{
|
|
if (message.DeviceId == Device.ID)
|
|
{
|
|
if (message.State == _state)
|
|
{
|
|
return;
|
|
}
|
|
_state = message.State;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
else
|
|
{
|
|
if (_state == DeviceCardState.Normal)
|
|
{
|
|
return;
|
|
}
|
|
_state = DeviceCardState.Normal;
|
|
await InvokeAsync(StateHasChanged);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private async Task HandleDeviceStateChanged(object subscriber, DeviceStateChangedMessage message)
|
|
{
|
|
if (message.Device.ID != Device.ID)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// TODO: It would be cool to decorate user-editable properties
|
|
// with a "UserEditable" attribute, then use a source generator
|
|
// to create/update a method that copies property values for
|
|
// those that do not have the attribute. We could do the same
|
|
// with reflection, but this method is called too frequently,
|
|
// and the performance hit would likely be significant.
|
|
|
|
// If the card is expanded, only update the immutable UI
|
|
// elements, so any changes to the form fields aren't lost.
|
|
if (IsExpanded)
|
|
{
|
|
Device.CurrentUser = message.Device.CurrentUser;
|
|
Device.Platform = message.Device.Platform;
|
|
Device.TotalStorage = message.Device.TotalStorage;
|
|
Device.UsedStorage = message.Device.UsedStorage;
|
|
Device.Drives = message.Device.Drives;
|
|
Device.CpuUtilization = message.Device.CpuUtilization;
|
|
Device.TotalMemory = message.Device.TotalMemory;
|
|
Device.UsedMemory = message.Device.UsedMemory;
|
|
Device.AgentVersion = message.Device.AgentVersion;
|
|
Device.LastOnline = message.Device.LastOnline;
|
|
Device.PublicIP = message.Device.PublicIP;
|
|
Device.MacAddresses = message.Device.MacAddresses;
|
|
}
|
|
else
|
|
{
|
|
Device = message.Device;
|
|
}
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private void ContextMenuOpening(MouseEventArgs args)
|
|
{
|
|
if (_state == DeviceCardState.Normal)
|
|
{
|
|
JsInterop.OpenWindow($"/device-details/{Device.ID}", "_blank");
|
|
}
|
|
}
|
|
|
|
private async Task ExpandCard(MouseEventArgs args)
|
|
{
|
|
if (_state == DeviceCardState.Expanded)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Messenger.Send(
|
|
new DeviceCardStateChangedMessage(Device.ID, DeviceCardState.Expanded),
|
|
CircuitConnection.ConnectionId);
|
|
|
|
JsInterop.ScrollToElement(_card);
|
|
|
|
await CircuitConnection.TriggerHeartbeat(Device.ID);
|
|
}
|
|
|
|
|
|
private string GetCardStateClass()
|
|
{
|
|
return $"{_state}".ToLower();
|
|
}
|
|
|
|
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)
|
|
{
|
|
_state = DeviceCardState.Normal;
|
|
}
|
|
}
|
|
private async Task HandleValidSubmit()
|
|
{
|
|
EnsureUserSet();
|
|
if (!DataService.DoesUserHaveAccessToDevice(Device.ID, User))
|
|
{
|
|
ToastService.ShowToast("Unauthorized.", classString: "bg-warning");
|
|
return;
|
|
}
|
|
|
|
await 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)
|
|
{
|
|
try
|
|
{
|
|
EnsureUserSet();
|
|
|
|
ToastService.ShowToast("File upload started.");
|
|
|
|
if (args.File.Size > AppConstants.MaxUploadFileSize)
|
|
{
|
|
var maxFileSize = AppConstants.MaxUploadFileSize / 1000 / 1000;
|
|
ToastService.ShowToast2($"File size exceeds the maximum allowed size of {maxFileSize}MB.", ToastType.Warning);
|
|
return;
|
|
}
|
|
|
|
var fileId = await DataService.AddSharedFile(args.File, User.OrganizationID, OnFileInputProgress);
|
|
var transferId = Guid.NewGuid().ToString();
|
|
var result = await CircuitConnection.TransferFileFromBrowserToAgent(Device.ID, transferId, [fileId]);
|
|
|
|
if (!result)
|
|
{
|
|
ToastService.ShowToast("Device not found.", classString: "bg-warning");
|
|
}
|
|
else
|
|
{
|
|
ToastService.ShowToast("File upload completed.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Error while uploading file to device.");
|
|
ToastService.ShowToast2("Failed to upload file", ToastType.Error);
|
|
}
|
|
finally
|
|
{
|
|
if (args.File.Name is not null)
|
|
{
|
|
_ = _fileUploadProgressLookup.TryRemove(args.File.Name, out _);
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 ShowAllDisks()
|
|
{
|
|
var disksString = JsonSerializer.Serialize(Device.Drives, JsonSerializerHelper.IndentedOptions);
|
|
|
|
void modalBody(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
|
|
{
|
|
builder.AddMarkupContent(0, $"<div style='white-space: pre'>{disksString}</div>");
|
|
}
|
|
|
|
ModalService.ShowModal($"All Disks for {Device.DeviceName}", modalBody);
|
|
}
|
|
|
|
private void StartChat()
|
|
{
|
|
var session = ChatCache.GetOrAdd(Device.ID, key =>
|
|
{
|
|
return new ChatSession()
|
|
{
|
|
DeviceId = key,
|
|
DeviceName = Device.DeviceName,
|
|
IsExpanded = true
|
|
};
|
|
});
|
|
|
|
session.IsExpanded = true;
|
|
Messenger.Send(new ChatSessionsChangedMessage(), CircuitConnection.ConnectionId);
|
|
}
|
|
|
|
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(
|
|
$"/Viewer" +
|
|
$"?mode=Unattended&sessionId={session.UnattendedSessionId}" +
|
|
$"&accessKey={session.AccessKey}" +
|
|
$"&viewonly={viewOnly}",
|
|
"_blank");
|
|
}
|
|
|
|
private void ToggleIsSelected(ChangeEventArgs args)
|
|
{
|
|
if (args.Value is not bool isSelected)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (isSelected)
|
|
{
|
|
SelectedCards.Add(Device.ID);
|
|
}
|
|
else
|
|
{
|
|
SelectedCards.Remove(Device.ID);
|
|
}
|
|
}
|
|
|
|
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 });
|
|
_state = DeviceCardState.Normal;
|
|
await ParentFrame.Refresh();
|
|
}
|
|
}
|
|
|
|
private async Task WakeDevice()
|
|
{
|
|
var result = await CircuitConnection.WakeDevice(Device);
|
|
if (result.IsSuccess)
|
|
{
|
|
ToastService.ShowToast2(
|
|
$"Wake command sent to peer devices.",
|
|
ToastType.Success);
|
|
}
|
|
else
|
|
{
|
|
ToastService.ShowToast2(
|
|
$"Wake command failed. Reason: {result.Reason}",
|
|
ToastType.Error);
|
|
}
|
|
}
|
|
} |