diff --git a/src/Languages/lang_en.json b/src/Languages/lang_en.json index b5e162bf09..7a21f3e8be 100644 --- a/src/Languages/lang_en.json +++ b/src/Languages/lang_en.json @@ -460,6 +460,112 @@ "This option WILL cause issues. Any operation incapable of elevating itself WILL FAIL. Install/update/uninstall as administrator will NOT WORK.": "This option WILL cause issues. Any operation incapable of elevating itself WILL FAIL. Install/update/uninstall as administrator will NOT WORK.", "Delegate package operations to the Devolutions Agent broker": "Delegate package operations to the Devolutions Agent broker", "When enabled, install, update and uninstall operations for supported package managers will be performed by the Devolutions Agent service instead of requesting UAC elevation. Operations will fail unless the agent is installed and running.": "When enabled, install, update and uninstall operations for supported package managers will be performed by the Devolutions Agent service instead of requesting UAC elevation. Operations will fail unless the agent is installed and running.", + "Inspect active package broker policy": "Inspect active package broker policy", + "Check Devolutions Agent reachability and review the policy currently enforced for package operations.": "Check Devolutions Agent reachability and review the policy currently enforced for package operations.", + "Active package broker policy": "Active package broker policy", + "Loading active package broker policy": "Loading active package broker policy", + "Contacting the Devolutions Agent service.": "Contacting the Devolutions Agent service.", + "Devolutions Agent is unavailable": "Devolutions Agent is unavailable", + "The package broker could not be reached. Verify that Devolutions Agent is installed and running, then refresh.": "The package broker could not be reached. Verify that Devolutions Agent is installed and running, then refresh.", + "Policy inspection is unsupported": "Policy inspection is unsupported", + "The installed Devolutions Agent is reachable but does not support active policy inspection. Update the Agent and try again.": "The installed Devolutions Agent is reachable but does not support active policy inspection. Update the Agent and try again.", + "Access to the active policy was denied": "Access to the active policy was denied", + "Devolutions Agent did not authorize UniGetUI to inspect the active package policy.": "Devolutions Agent did not authorize UniGetUI to inspect the active package policy.", + "The active policy is unavailable": "The active policy is unavailable", + "Devolutions Agent supports policy inspection but could not provide the active policy. Review the Agent configuration and try again.": "Devolutions Agent supports policy inspection but could not provide the active policy. Review the Agent configuration and try again.", + "The policy response is invalid": "The policy response is invalid", + "Devolutions Agent returned a malformed or incompatible policy response.": "Devolutions Agent returned a malformed or incompatible policy response.", + "Policy inspection is available on Windows only": "Policy inspection is available on Windows only", + "This page cannot contact the Windows Devolutions Agent service on the current platform.": "This page cannot contact the Windows Devolutions Agent service on the current platform.", + "Connected to Devolutions Agent": "Connected to Devolutions Agent", + "The active package broker policy was loaded successfully.": "The active package broker policy was loaded successfully.", + "Refresh": "Refresh", + "Refresh active policy": "Refresh active policy", + "Policy metadata": "Policy metadata", + "Enforcement": "Enforcement", + "Rules": "Rules", + "The active policy contains no rules.": "The active policy contains no rules.", + "Canonical policy JSON": "Canonical policy JSON", + "Copy canonical policy JSON to clipboard": "Copy canonical policy JSON to clipboard", + "Server version": "Server version", + "Policy ID": "Policy ID", + "Revision": "Revision", + "Policy version": "Policy version", + "Published": "Published", + "Valid from": "Valid from", + "Valid until": "Valid until", + "Description": "Description", + "Support URL": "Support URL", + "Default decision": "Default decision", + "Rule precedence": "Rule precedence", + "Audit mode": "Audit mode", + "Rule {0}: {1}": "Rule {0}: {1}", + "Rule ID": "Rule ID", + "Priority": "Priority", + "Decision": "Decision", + "Reason": "Reason", + "Match criteria": "Match criteria", + "Constraints": "Constraints", + "Package managers": "Package managers", + "Package identifiers": "Package identifiers", + "Package names": "Package names", + "Versions": "Versions", + "Version range": "Version range", + "Scopes": "Scopes", + "Architectures": "Architectures", + "Elevation": "Elevation", + "Interactive": "Interactive", + "Prerelease": "Prerelease", + "Has custom parameters": "Has custom parameters", + "Has custom install location": "Has custom install location", + "Has pre/post commands": "Has pre/post commands", + "Has kill-before-operation": "Has kill-before-operation", + "Has uninstall previous": "Has uninstall previous", + "Allow interactive": "Allow interactive", + "Allow skip hash check": "Allow skip hash check", + "Allow prerelease": "Allow prerelease", + "Allow custom install location": "Allow custom install location", + "Allowed install location patterns": "Allowed install location patterns", + "Allow custom parameters": "Allow custom parameters", + "Allowed custom parameters": "Allowed custom parameters", + "Allowed custom parameter patterns": "Allowed custom parameter patterns", + "Denied custom parameters": "Denied custom parameters", + "Allow pre/post commands": "Allow pre/post commands", + "Allow kill-before-operation": "Allow kill-before-operation", + "Allow uninstall previous": "Allow uninstall previous", + "Allow upgrade": "Allow upgrade", + "Not set": "Not set", + "Any": "Any", + "None": "None", + "{0} to {1}; include prerelease: {2}": "{0} to {1}; include prerelease: {2}", + "Allow": "Allow", + "Deny": "Deny", + "PriorityThenDeny": "Priority, then deny", + "Winget": "WinGet", + "PowerShell": "PowerShell", + "PowerShell7": "PowerShell 7", + "Apt": "APT", + "Bun": "Bun", + "Cargo": "Cargo", + "Chocolatey": "Chocolatey", + "Dnf": "DNF", + "Dotnet": ".NET Tool", + "Flatpak": "Flatpak", + "Homebrew": "Homebrew", + "Npm": "npm", + "Pacman": "Pacman", + "Pip": "Pip", + "Scoop": "Scoop", + "Snap": "Snap", + "Vcpkg": "Vcpkg", + "User": "User", + "Machine": "Machine", + "X86": "x86", + "X64": "x64", + "Arm64": "ARM64", + "Neutral": "Neutral", + "Standard": "Standard", + "Elevated": "Elevated", "Allow custom command-line arguments": "Allow custom command-line arguments", "Custom command-line arguments can change the way in which programs are installed, upgraded or uninstalled, in a way UniGetUI cannot control. Using custom command-lines can break packages. Proceed with caution.": "Custom command-line arguments can change the way in which programs are installed, upgraded or uninstalled, in a way UniGetUI cannot control. Using custom command-lines can break packages. Proceed with caution.", "Ignore custom pre-install and post-install commands when importing packages from a bundle": "Ignore custom pre-install and post-install commands when importing packages from a bundle", diff --git a/src/UniGetUI.Avalonia/Infrastructure/SettingsSearchIndex.cs b/src/UniGetUI.Avalonia/Infrastructure/SettingsSearchIndex.cs index 16bf233aa4..9beb29fed0 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/SettingsSearchIndex.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/SettingsSearchIndex.cs @@ -27,7 +27,12 @@ public sealed class SettingsSearchResult /// public static class SettingsSearchIndex { - private sealed record Entry(string Title, string[] Keywords, Type PageType, string? Anchor); + private sealed record Entry( + string Title, + string[] Keywords, + Type PageType, + string? Anchor, + bool WindowsOnly = false); // Order matters only as a tie-break for equally-ranked matches. private static readonly Entry[] Entries = @@ -109,6 +114,7 @@ private sealed record Entry(string Title, string[] Keywords, Type PageType, stri new("Ask for administrator privileges once for each batch of operations", ["administrator", "admin rights", "elevation", "uac", "batch"], typeof(Administrator), "AdminElevationCard"), new("Ask only once for administrator privileges", ["admin once", "cache admin rights"], typeof(Administrator), "CacheAdminOnceCard"), new("Prohibit any kind of Elevation via UniGetUI Elevator or GSudo", ["prohibit elevation", "no elevation"], typeof(Administrator), "ProhibitElevationCard"), + new("Inspect active package broker policy", ["policy", "package broker", "devolutions agent", "rules", "enforcement"], typeof(AgentPolicyInspector), null, WindowsOnly: true), new("Allow custom command-line arguments", ["command line arguments", "cli arguments"], typeof(Administrator), "AdminRestrictionsOpsCard"), new("Ignore custom pre-install and post-install commands when importing packages from a bundle", ["pre-install commands", "post-install commands"], typeof(Administrator), "PrePostCommandCard"), new("Allow changing the paths for package manager executables", ["manager paths", "executable path"], typeof(Administrator), "AdminManagerPathsCard"), @@ -132,6 +138,11 @@ private sealed record Entry(string Title, string[] Keywords, Type PageType, stri private const int NoMatch = int.MaxValue; public static IReadOnlyList Search(string query, int limit = 8) + { + return Search(query, limit, OperatingSystem.IsWindows()); + } + + internal static IReadOnlyList Search(string query, int limit, bool isWindows) { var queryWords = Tokenize(query); if (queryWords.Count == 0) return []; @@ -140,6 +151,8 @@ public static IReadOnlyList Search(string query, int limit foreach (var e in Entries) { + if (e.WindowsOnly && !isWindows) continue; + int score = Rank(queryWords, CoreTools.Translate(e.Title), e.Title, e.Keywords); if (score < NoMatch) scored.Add((score, new SettingsSearchResult @@ -263,6 +276,7 @@ void Flush() nameof(Internet) => "Internet connection settings", nameof(Backup) => "Package backup", nameof(Administrator) => "Administrator rights and other dangerous settings", + nameof(AgentPolicyInspector) => "Active package broker policy", nameof(Experimental) => "Experimental settings and developer options", _ => "UniGetUI Settings", }); diff --git a/src/UniGetUI.Avalonia/InternalsVisibleTo.cs b/src/UniGetUI.Avalonia/InternalsVisibleTo.cs new file mode 100644 index 0000000000..f5236ec50a --- /dev/null +++ b/src/UniGetUI.Avalonia/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("UniGetUI.Tests")] diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index 6f20dba70c..471725a2f1 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -211,6 +211,7 @@ + diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs index 3abd7ca894..ebbef0e01a 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AdministratorViewModel.cs @@ -1,6 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Avalonia.Views.Pages.SettingsPages; using UniGetUI.Core.SettingsEngine; using UniGetUI.Core.SettingsEngine.SecureSettings; using UniGetUI.Core.Tools; @@ -10,6 +11,7 @@ namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; public partial class AdministratorViewModel : ViewModelBase { public event EventHandler? RestartRequired; + public event EventHandler? NavigationRequested; // ── Warning banner strings ──────────────────────────────────────────── public string WarningTitle { get; } = CoreTools.Translate("Warning") + "!"; @@ -67,4 +69,8 @@ private void RefreshPrePostState() { IsPrePostCommandsEnabled = SecureSettings.Get(SecureSettings.K.AllowPrePostOpCommand); } + + [RelayCommand] + private void NavigateToAgentPolicyInspector() => + NavigationRequested?.Invoke(this, typeof(AgentPolicyInspector)); } diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AgentPolicyInspectorViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AgentPolicyInspectorViewModel.cs new file mode 100644 index 0000000000..65c4db60dd --- /dev/null +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/AgentPolicyInspectorViewModel.cs @@ -0,0 +1,343 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Devolutions.Now.Policy.Api; +using Devolutions.Now.Policy.Model; +using UniGetUI.Avalonia.ViewModels; +using UniGetUI.Core.Tools; +using UniGetUI.PackageEngine.AgentBroker; +using PolicyArchitecture = Devolutions.Now.Policy.Model.Architecture; +using PolicyDecision = Devolutions.Now.Policy.Model.Decision; +using PolicyElevation = Devolutions.Now.Policy.Model.Elevation; +using PolicyManagerName = Devolutions.Now.Policy.Model.ManagerName; +using PolicyOperation = Devolutions.Now.Policy.Model.Operation; +using PolicyScope = Devolutions.Now.Policy.Model.Scope; + +namespace UniGetUI.Avalonia.ViewModels.Pages.SettingsPages; + +public sealed record PolicyDetailRow(string Label, string Value) +{ + public string AutomationName => $"{Label}: {Value}"; +} + +public sealed class PolicyRuleViewModel +{ + public required string AutomationName { get; init; } + public required string Id { get; init; } + public required string Enabled { get; init; } + public required string Priority { get; init; } + public required string Decision { get; init; } + public required string Reason { get; init; } + public required IReadOnlyList MatchRows { get; init; } + public required IReadOnlyList ConstraintRows { get; init; } +} + +public partial class AgentPolicyInspectorViewModel : ViewModelBase, IDisposable +{ + private readonly IBrokerPolicyInspector _inspector; + private readonly CancellationTokenSource _lifetimeCancellation = new(); + private CancellationTokenSource? _refreshCancellation; + private long _refreshGeneration; + private int _isDisposed; + + public InfoBarViewModel Status { get; } = new() + { + IsClosable = false, + IsOpen = true, + }; + + public ObservableCollection MetadataRows { get; } = []; + public ObservableCollection EnforcementRows { get; } = []; + public ObservableCollection Rules { get; } = []; + + [ObservableProperty] private bool _isLoading; + [ObservableProperty] private bool _hasPolicy; + [ObservableProperty] private bool _hasNoRules; + [ObservableProperty] private string _rawJson = ""; + + public event EventHandler? CopyTextRequested; + + public AgentPolicyInspectorViewModel() + : this(new BrokerPolicyInspector()) + { + } + + public AgentPolicyInspectorViewModel(IBrokerPolicyInspector inspector) + { + _inspector = inspector; + SetStatus( + CoreTools.Translate("Loading active package broker policy"), + CoreTools.Translate("Contacting the Devolutions Agent service."), + InfoBarSeverity.Informational); + } + + public Task LoadAsync() => RefreshAsync(); + + [RelayCommand(AllowConcurrentExecutions = true)] + private async Task RefreshAsync() + { + if (Volatile.Read(ref _isDisposed) != 0) return; + + long generation = Interlocked.Increment(ref _refreshGeneration); + var cancellation = CancellationTokenSource.CreateLinkedTokenSource(_lifetimeCancellation.Token); + CancellationTokenSource? previous = Interlocked.Exchange(ref _refreshCancellation, cancellation); + previous?.Cancel(); + previous?.Dispose(); + + IsLoading = true; + HasPolicy = false; + SetStatus( + CoreTools.Translate("Loading active package broker policy"), + CoreTools.Translate("Contacting the Devolutions Agent service."), + InfoBarSeverity.Informational); + + try + { + BrokerPolicyInspectionResult result = + await _inspector.InspectAsync(cancellation.Token); + if (!CanApply(generation, cancellation)) return; + + ApplyResult(result); + } + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + } + finally + { + if (CanApply(generation, cancellation)) + { + IsLoading = false; + } + } + } + + [RelayCommand] + private void CopyRawJson() + { + if (!string.IsNullOrEmpty(RawJson)) + { + CopyTextRequested?.Invoke(this, RawJson); + } + } + + private bool CanApply(long generation, CancellationTokenSource cancellation) + { + return Volatile.Read(ref _isDisposed) == 0 + && !cancellation.IsCancellationRequested + && generation == Volatile.Read(ref _refreshGeneration); + } + + private void ApplyResult(BrokerPolicyInspectionResult result) + { + ClearPolicy(); + + switch (result.Status) + { + case BrokerPolicyInspectionStatus.Connected when result.Response is not null: + ApplyPolicy(result.Response, result.CanonicalJson ?? ""); + break; + case BrokerPolicyInspectionStatus.AgentUnavailable: + SetStatus( + CoreTools.Translate("Devolutions Agent is unavailable"), + CoreTools.Translate("The package broker could not be reached. Verify that Devolutions Agent is installed and running, then refresh."), + InfoBarSeverity.Error); + break; + case BrokerPolicyInspectionStatus.Unsupported: + SetStatus( + CoreTools.Translate("Policy inspection is unsupported"), + CoreTools.Translate("The installed Devolutions Agent is reachable but does not support active policy inspection. Update the Agent and try again."), + InfoBarSeverity.Warning); + break; + case BrokerPolicyInspectionStatus.AccessDenied: + SetStatus( + CoreTools.Translate("Access to the active policy was denied"), + CoreTools.Translate("Devolutions Agent did not authorize UniGetUI to inspect the active package policy."), + InfoBarSeverity.Error); + break; + case BrokerPolicyInspectionStatus.PolicyUnavailable: + SetStatus( + CoreTools.Translate("The active policy is unavailable"), + CoreTools.Translate("Devolutions Agent supports policy inspection but could not provide the active policy. Review the Agent configuration and try again."), + InfoBarSeverity.Error); + break; + case BrokerPolicyInspectionStatus.InvalidResponse: + SetStatus( + CoreTools.Translate("The policy response is invalid"), + CoreTools.Translate("Devolutions Agent returned a malformed or incompatible policy response."), + InfoBarSeverity.Error); + break; + case BrokerPolicyInspectionStatus.UnsupportedPlatform: + SetStatus( + CoreTools.Translate("Policy inspection is available on Windows only"), + CoreTools.Translate("This page cannot contact the Windows Devolutions Agent service on the current platform."), + InfoBarSeverity.Warning); + break; + default: + SetStatus( + CoreTools.Translate("The policy response is invalid"), + CoreTools.Translate("Devolutions Agent returned a malformed or incompatible policy response."), + InfoBarSeverity.Error); + break; + } + } + + private void ApplyPolicy(PolicyResponse response, string canonicalJson) + { + PolicyDocument policy = response.Policy; + PolicyMetadata metadata = policy.Metadata; + + MetadataRows.Add(Row("Server version", Value(response.Server.ServerVersion))); + MetadataRows.Add(Row("Policy ID", Value(metadata.Id))); + MetadataRows.Add(Row("Publisher", Value(metadata.Publisher))); + MetadataRows.Add(Row("Revision", metadata.Revision.ToString(CultureInfo.CurrentCulture))); + MetadataRows.Add(Row("Policy version", Value(policy.PolicyVersion))); + MetadataRows.Add(Row("Published", FormatDate(metadata.PublishedAt))); + MetadataRows.Add(Row("Valid from", FormatDate(metadata.ValidFrom))); + MetadataRows.Add(Row("Valid until", FormatDate(metadata.ValidUntil))); + MetadataRows.Add(Row("Description", Value(metadata.Description))); + MetadataRows.Add(Row("Support URL", Value(metadata.SupportUrl))); + + EnforcementRows.Add(Row("Default decision", TranslateEnum(policy.Enforcement.DefaultDecision))); + EnforcementRows.Add(Row("Rule precedence", TranslateEnum(policy.Enforcement.RulePrecedence))); + EnforcementRows.Add(Row("Audit mode", FormatNullableBoolean(policy.Enforcement.AuditMode))); + + for (int index = 0; index < policy.Rules.Count; index++) + { + Rules.Add(BuildRule(policy.Rules[index], index)); + } + + RawJson = canonicalJson; + HasNoRules = Rules.Count == 0; + HasPolicy = true; + SetStatus( + CoreTools.Translate("Connected to Devolutions Agent"), + CoreTools.Translate("The active package broker policy was loaded successfully."), + InfoBarSeverity.Success); + } + + private static PolicyRuleViewModel BuildRule(PolicyRule rule, int index) + { + PolicyMatch match = rule.Match; + PolicyConstraints? constraints = rule.Constraints; + + return new PolicyRuleViewModel + { + AutomationName = CoreTools.Translate("Rule {0}: {1}", index + 1, Value(rule.Id)), + Id = Value(rule.Id), + Enabled = FormatBoolean(rule.Enabled), + Priority = rule.Priority.ToString(CultureInfo.CurrentCulture), + Decision = TranslateEnum(rule.Decision), + Reason = Value(rule.Reason), + MatchRows = + [ + Row("Operations", FormatEnumList(match.Operations)), + Row("Package managers", FormatEnumList(match.Managers)), + Row("Sources", FormatList(match.Sources, anyWhenEmpty: true)), + Row("Package identifiers", FormatList(match.PackageIdentifiers, anyWhenEmpty: true)), + Row("Package names", FormatList(match.PackageNames, anyWhenEmpty: true)), + Row("Versions", FormatList(match.Versions, anyWhenEmpty: true)), + Row("Version range", FormatVersionRange(match.VersionRange)), + Row("Scopes", FormatEnumList(match.Scopes)), + Row("Architectures", FormatEnumList(match.Architectures)), + Row("Elevation", FormatEnumList(match.Elevation)), + Row("Interactive", FormatBooleanList(match.Interactive)), + Row("Skip hash check", FormatBooleanList(match.SkipHashCheck)), + Row("Prerelease", FormatBooleanList(match.PreRelease)), + Row("Has custom parameters", FormatBooleanList(match.HasCustomParameters)), + Row("Has custom install location", FormatBooleanList(match.HasCustomInstallLocation)), + Row("Has pre/post commands", FormatBooleanList(match.HasPrePostCommands)), + Row("Has kill-before-operation", FormatBooleanList(match.HasKillBeforeOperation)), + Row("Has uninstall previous", FormatBooleanList(match.HasUninstallPrevious)), + ], + ConstraintRows = constraints is null + ? [Row("Constraints", CoreTools.Translate("Not set"))] + : + [ + Row("Allow interactive", FormatBoolean(constraints.AllowInteractive)), + Row("Allow skip hash check", FormatBoolean(constraints.AllowSkipHashCheck)), + Row("Allow prerelease", FormatBoolean(constraints.AllowPreRelease)), + Row("Allow custom install location", FormatBoolean(constraints.AllowCustomInstallLocation)), + Row("Allowed install location patterns", FormatList(constraints.AllowedInstallLocationPatterns)), + Row("Allow custom parameters", FormatBoolean(constraints.AllowCustomParameters)), + Row("Allowed custom parameters", FormatList(constraints.AllowedCustomParameters)), + Row("Allowed custom parameter patterns", FormatList(constraints.AllowedCustomParameterPatterns)), + Row("Denied custom parameters", FormatList(constraints.DeniedCustomParameters)), + Row("Allow pre/post commands", FormatBoolean(constraints.AllowPrePostCommands)), + Row("Allow kill-before-operation", FormatBoolean(constraints.AllowKillBeforeOperation)), + Row("Allow uninstall previous", FormatBoolean(constraints.AllowUninstallPrevious)), + Row("Allow upgrade", FormatBoolean(constraints.AllowUpgrade)), + ], + }; + } + + private static PolicyDetailRow Row(string label, string value) => + new(CoreTools.Translate(label), value); + + private static string FormatDate(DateTimeOffset? value) => + value?.ToLocalTime().ToString("g", CultureInfo.CurrentCulture) + ?? CoreTools.Translate("Not set"); + + private static string FormatBoolean(bool value) => + CoreTools.Translate(value ? "Yes" : "No"); + + private static string FormatNullableBoolean(bool? value) => + value.HasValue ? FormatBoolean(value.Value) : CoreTools.Translate("Not set"); + + private static string FormatBooleanList(IEnumerable values) => + FormatList(values.Select(FormatBoolean), anyWhenEmpty: true); + + private static string FormatEnumList(IEnumerable values) where T : struct, Enum => + FormatList(values.Select(TranslateEnum), anyWhenEmpty: true); + + private static string FormatList(IEnumerable values, bool anyWhenEmpty = false) + { + string[] items = values.Where(value => !string.IsNullOrWhiteSpace(value)).ToArray(); + return items.Length == 0 + ? CoreTools.Translate(anyWhenEmpty ? "Any" : "None") + : string.Join(", ", items); + } + + private static string FormatVersionRange(VersionRange? range) + { + if (range is null) return CoreTools.Translate("Any"); + + return CoreTools.Translate( + "{0} to {1}; include prerelease: {2}", + Value(range.MinVersion, "Any"), + Value(range.MaxVersion, "Any"), + FormatBoolean(range.IncludePrerelease)); + } + + private static string TranslateEnum(T value) where T : struct, Enum => + CoreTools.Translate(value.ToString()); + + private static string Value(string? value, string fallback = "Not set") => + string.IsNullOrWhiteSpace(value) ? CoreTools.Translate(fallback) : value; + + private void ClearPolicy() + { + MetadataRows.Clear(); + EnforcementRows.Clear(); + Rules.Clear(); + RawJson = ""; + HasPolicy = false; + HasNoRules = false; + } + + private void SetStatus(string title, string message, InfoBarSeverity severity) + { + Status.Title = title; + Status.Message = message; + Status.Severity = severity; + Status.IsOpen = true; + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _isDisposed, 1) != 0) return; + + _lifetimeCancellation.Cancel(); + Interlocked.Exchange(ref _refreshCancellation, null)?.Cancel(); + } +} diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml index 9a52f5002c..abd9afb56b 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Administrator.axaml @@ -90,6 +90,15 @@ Text="{t:Translate Delegate package operations to the Devolutions Agent broker}" WarningText="{t:Translate Text='When enabled, install, update and uninstall operations for supported package managers will be performed by the Devolutions Agent service instead of requesting UAC elevation. Operations will fail unless the agent is installed and running.'}" CornerRadius="8"/> + + + + CoreTools.Translate("Administrator rights and other dangerous settings"); public event EventHandler? RestartRequired; - public event EventHandler? NavigationRequested { add { } remove { } } + public event EventHandler? NavigationRequested; public Administrator() { DataContext = new AdministratorViewModel(); InitializeComponent(); VM.RestartRequired += (s, e) => RestartRequired?.Invoke(s, e); + VM.NavigationRequested += (s, e) => NavigationRequested?.Invoke(s, e); } } diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/AgentPolicyInspector.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/AgentPolicyInspector.axaml new file mode 100644 index 0000000000..00524aa01e --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/AgentPolicyInspector.axaml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + +