Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Globalization;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;
Expand Down Expand Up @@ -86,7 +85,7 @@ protected override async Task OnParametersSetAsync()
{
if (RememberOrientation)
{
var orientationResult = await LocalStore.SafeGetAsync<Orientation>(GetOrientationStorageKey());
var orientationResult = await LocalStore.GetUnprotectedAsync<Orientation>(GetOrientationStorageKey());
if (orientationResult.Success)
{
Orientation = orientationResult.Value;
Expand All @@ -95,7 +94,7 @@ protected override async Task OnParametersSetAsync()

if (RememberSize)
{
var panel1FractionResult = await LocalStore.SafeGetAsync<float>(GetSizeStorageKey());
var panel1FractionResult = await LocalStore.GetUnprotectedAsync<float>(GetSizeStorageKey());
if (panel1FractionResult.Success)
{
SetPanelSizes(panel1FractionResult.Value);
Expand Down Expand Up @@ -128,12 +127,12 @@ private async Task HandleToggleOrientation()

if (RememberOrientation)
{
await LocalStore.SetAsync(GetOrientationStorageKey(), Orientation);
await LocalStore.SetUnprotectedAsync(GetOrientationStorageKey(), Orientation);
}

if (RememberSize)
{
var panel1FractionResult = await LocalStore.SafeGetAsync<float>(GetSizeStorageKey());
var panel1FractionResult = await LocalStore.GetUnprotectedAsync<float>(GetSizeStorageKey());
if (panel1FractionResult.Success)
{
SetPanelSizes(panel1FractionResult.Value);
Expand Down Expand Up @@ -170,7 +169,7 @@ private async Task HandleSplitterResize(SplitterResizedEventArgs args)

private async Task SaveSizeToStorage(float panel1Fraction)
{
await LocalStore.SetAsync(GetSizeStorageKey(), panel1Fraction);
await LocalStore.SetUnprotectedAsync(GetSizeStorageKey(), panel1Fraction);
}

private void ResetPanelSizes()
Expand Down Expand Up @@ -293,13 +292,13 @@ static void GetPanelSizes(
private string GetSizeStorageKey()
{
var viewKey = ViewKey ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
return $"SplitterSize_{Orientation}_{viewKey}";
return $"Aspire_SplitterSize_{Orientation}_{viewKey}";
}

private string GetOrientationStorageKey()
{
var viewKey = ViewKey ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
return $"SplitterOrientation_{viewKey}";
return $"Aspire_SplitterOrientation_{viewKey}";
}

public void Dispose()
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public sealed partial class ConsoleLogs : ComponentBase, IAsyncDisposable, IPage
public ConsoleLogsViewModel PageViewModel { get; set; } = null!;

public string BasePath => DashboardUrls.ConsoleLogBasePath;
public string SessionStorageKey => "ConsoleLogs_PageState";
public string SessionStorageKey => "Aspire_ConsoleLogs_PageState";

protected override async Task OnInitializedAsync()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public partial class Metrics : IDisposable, IPageWithSessionAndUrlState<Metrics.
private Subscription? _metricsSubscription;

public string BasePath => DashboardUrls.MetricsBasePath;
public string SessionStorageKey => "Metrics_PageState";
public string SessionStorageKey => "Aspire_Metrics_PageState";
public MetricsViewModel PageViewModel { get; set; } = null!;

[Parameter]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public partial class StructuredLogs : IPageWithSessionAndUrlState<StructuredLogs
private GridColumnManager _manager = null!;

public string BasePath => DashboardUrls.StructuredLogsBasePath;
public string SessionStorageKey => "StructuredLogs_PageState";
public string SessionStorageKey => "Aspire_StructuredLogs_PageState";
public StructuredLogsPageViewModel PageViewModel { get; set; } = null!;

[Inject]
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/Traces.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public partial class Traces : IPageWithSessionAndUrlState<TracesPageViewModel, T
private AspirePageContentLayout? _contentLayout;
private GridColumnManager _manager = null!;

public string SessionStorageKey => "Traces_PageState";
public string SessionStorageKey => "Aspire_Traces_PageState";
public string BasePath => DashboardUrls.TracesBasePath;
public TracesPageViewModel PageViewModel { get; set; } = null!;

Expand Down
4 changes: 3 additions & 1 deletion src/Aspire.Dashboard/Model/BrowserStorage/ILocalStorage.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Model.BrowserStorage;

public interface ILocalStorage : IBrowserStorage
{
Task<StorageResult<T>> GetUnprotectedAsync<T>(string key);
Task SetUnprotectedAsync<T>(string key, T value);
}
37 changes: 35 additions & 2 deletions src/Aspire.Dashboard/Model/BrowserStorage/LocalBrowserStorage.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.JSInterop;

namespace Aspire.Dashboard.Model.BrowserStorage;

public class LocalBrowserStorage : BrowserStorageBase, ILocalStorage
{
public LocalBrowserStorage(ProtectedLocalStorage protectedLocalStorage) : base(protectedLocalStorage)
private static readonly JsonSerializerOptions s_options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
};

private readonly IJSRuntime _jsRuntime;

public LocalBrowserStorage(IJSRuntime jsRuntime, ProtectedLocalStorage protectedLocalStorage) : base(protectedLocalStorage)
{
_jsRuntime = jsRuntime;
}

public async Task<StorageResult<T>> GetUnprotectedAsync<T>(string key)
{
var json = await GetJsonAsync(key).ConfigureAwait(false);

return json == null ?
new StorageResult<T>(false, default) :
new StorageResult<T>(true, JsonSerializer.Deserialize<T>(json, s_options));
}

public async Task SetUnprotectedAsync<T>(string key, T value)
{
var json = JsonSerializer.Serialize(value, s_options);

await SetJsonAsync(key, json).ConfigureAwait(false);
}

private ValueTask SetJsonAsync(string key, string json)
=> _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, json);

private ValueTask<string?> GetJsonAsync(string key)
=> _jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
Comment on lines +55 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two methods return ValueTask. I'm not sure how often these would return completed tasks, but perhaps we also use value tasks on the new methods on ILocalStorage.

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
Expand Down
29 changes: 0 additions & 29 deletions src/Aspire.Dashboard/Utils/IBrowserStorageExtensions.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@ public sealed class TestLocalStorage : ILocalStorage
{
public Task<StorageResult<T>> GetAsync<T>(string key)
{
return Task.FromResult<StorageResult<T>>(new StorageResult<T>(Success: false, Value: default));
return Task.FromResult(new StorageResult<T>(Success: false, Value: default));
}

public Task<StorageResult<T>> GetUnprotectedAsync<T>(string key)
{
return Task.FromResult(new StorageResult<T>(Success: false, Value: default));
}

public Task SetAsync<T>(string key, T value)
{
return Task.CompletedTask;
}

public Task SetUnprotectedAsync<T>(string key, T value)
{
return Task.CompletedTask;
}
}
134 changes: 134 additions & 0 deletions tests/Aspire.Dashboard.Tests/LocalBrowserStorageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Aspire.Dashboard.Model.BrowserStorage;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.JSInterop;
using Xunit;

namespace Aspire.Dashboard.Tests;

public class LocalBrowserStorageTests
{
[Theory]
[InlineData(123, "123")]
[InlineData("Hello world", @"""Hello world""")]
[InlineData(null, "null")]
public async Task SetUnprotectedAsync_JSInvokedWithJson(object? value, string result)
{
// Arrange
string? identifier = null;
object?[]? args = null;

var testJsonRuntime = new TestJSRuntime();
testJsonRuntime.OnInvoke = r =>
{
(identifier, args) = r;
return default;
};
var localStorage = new LocalBrowserStorage(testJsonRuntime, new ProtectedLocalStorage(testJsonRuntime, new TestDataProtector()));

// Act
await localStorage.SetUnprotectedAsync("MyKey", value);

// Assert
Assert.Equal("localStorage.setItem", identifier);
Assert.NotNull(args);
Assert.Equal("MyKey", args[0]);
Assert.Equal(result, args[1]);
}

[Fact]
public async Task GetUnprotectedAsync_HasValue_Success()
{
// Arrange
string? identifier = null;
object?[]? args = null;

var testJsonRuntime = new TestJSRuntime();
testJsonRuntime.OnInvoke = r =>
{
(identifier, args) = r;
return "123";
};
var localStorage = new LocalBrowserStorage(testJsonRuntime, new ProtectedLocalStorage(testJsonRuntime, new TestDataProtector()));

// Act
var result = await localStorage.GetUnprotectedAsync<int>("MyKey");

// Assert
Assert.True(result.Success);
Assert.Equal(123, result.Value);
Assert.Equal("localStorage.getItem", identifier);
Assert.NotNull(args);
Assert.Equal("MyKey", args[0]);
}

[Fact]
public async Task GetUnprotectedAsync_NoValue_Failure()
{
// Arrange
string? identifier = null;
object?[]? args = null;

var testJsonRuntime = new TestJSRuntime();
testJsonRuntime.OnInvoke = r =>
{
(identifier, args) = r;
return default;
};
var localStorage = new LocalBrowserStorage(testJsonRuntime, new ProtectedLocalStorage(testJsonRuntime, new TestDataProtector()));

// Act
var result = await localStorage.GetUnprotectedAsync<int>("MyKey");

// Assert
Assert.False(result.Success);
Assert.Equal("localStorage.getItem", identifier);
Assert.NotNull(args);
Assert.Equal("MyKey", args[0]);
}

private sealed class TestJSRuntime : IJSRuntime
{
public Func<(string Identifier, object?[]? Args), object?>? OnInvoke { get; set; }

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, object?[]? args)
{
if (OnInvoke?.Invoke((identifier, args)) is TValue result)
{
return ValueTask.FromResult(result);
}
return default;
}

public ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object?[]? args)
{
if (OnInvoke?.Invoke((identifier, args)) is TValue result)
{
return ValueTask.FromResult(result);
}
return default;
}
}

private sealed class TestDataProtector : IDataProtector
{
public IDataProtector CreateProtector(string purpose)
{
throw new NotImplementedException();
}

public byte[] Protect(byte[] plaintext)
{
throw new NotImplementedException();
}

public byte[] Unprotect(byte[] protectedData)
{
throw new NotImplementedException();
}
}
}