Merge pull request #71 from lanedirt/55-add-client-side-database-sync-indicator-to-ui

Add client side database sync indicator to UI
This commit is contained in:
Leendert de Borst
2024-06-28 02:28:24 -07:00
committed by GitHub
15 changed files with 341 additions and 181 deletions

View File

@@ -60,6 +60,9 @@
</ItemGroup>
<ItemGroup>
<Content Update="Layout\DbStatusIndicator.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="wwwroot\appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

View File

@@ -5,7 +5,7 @@
@inject NavigationManager NavigationManager
@inject AuthService AuthService
@inject GlobalNotificationService GlobalNotificationService
@inject AliasClientDbService AliasClientDbService
@inject DbService DbService
@inject ILocalStorageService LocalStorage
@using System.Text.Json
@using AliasVault.Shared.Models
@@ -145,7 +145,7 @@
AuthService.StoreEncryptionKey(passwordHash);
// Try to retrieve vault again from remote.
await AliasClientDbService.GetDbContextAsync();
await DbService.GetDbContextAsync();
// Redirect to home page.
await AuthStateProvider.GetAuthenticationStateAsync();

View File

@@ -0,0 +1,22 @@
<div role="status" class="px-2" title="@Title">
<svg aria-hidden="true" class="inline w-7 h-7 text-gray-200 @(Spinning ? "animate-spin fill-primary-600" : "") dark:text-gray-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
</svg>
<span class="sr-only">Loading...</span>
</div>
@code {
/// <summary>
/// Optional title of the loading indicator.
/// </summary>
[Parameter]
public string Title { get; set; } = string.Empty;
/// <summary>
/// Set spinning to false to stop the animation.
/// </summary>
[Parameter]
public bool Spinning { get; set; } = true;
}

View File

@@ -0,0 +1,65 @@
@implements IDisposable
@inject DbService DbService
@if (Loading)
{
<div class="flex items-center justify-center">
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" />
</div>
}
else
{
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" Spinning="false" />
}
<!--
<p>Message: @DbService.GetState().CurrentState.Message</p>
<p>Last Updated: @DbService.GetState().CurrentState.LastUpdated</p>
-->
@code {
private bool Loading { get; set; } = false;
private string Message { get; set; } = "";
private string LoadingIndicatorMessage { get; set; } = "";
/// <inheritdoc />
protected override void OnInitialized()
{
DbService.GetState().StateChanged += OnDatabaseStateChanged;
}
private async void OnDatabaseStateChanged(object? sender, DbServiceState.DatabaseState newState)
{
await InvokeAsync(StateHasChanged);
if (newState.Status == DbServiceState.DatabaseStatus.Saving)
{
// Show loading indicator for at least 0.5 seconds even if the save operation is faster.
Message = "Saving...";
await ShowLoadingIndicatorAsync();
}
else if (newState.Status == DbServiceState.DatabaseStatus.Loading)
{
Message = "Loading...";
await ShowLoadingIndicatorAsync();
}
LoadingIndicatorMessage = Message + " - " + newState.LastUpdated;
}
private async Task ShowLoadingIndicatorAsync()
{
Loading = true;
StateHasChanged();
await Task.Delay(800);
Loading = false;
StateHasChanged();
}
/// <summary>
/// Dispose method.
/// </summary>
public void Dispose()
{
DbService.GetState().StateChanged -= OnDatabaseStateChanged;
}
}

View File

@@ -1,39 +0,0 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">AliasVault</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@@ -24,10 +24,8 @@
</ul>
</div>
</div>
<div class="flex justify-between items-center lg:order-2">
<div class="mr-3 -mb-1 hidden sm:block">
<span></span>
</div>
<div class="flex justify-end items-center lg:order-2">
<DbStatusIndicator />
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
@@ -93,6 +91,25 @@
private bool isMobileMenuOpen = false;
private string _username { get; set; } = "";
/// <summary>
/// Close the menu.
/// </summary>
[JSInvokable]
public void CloseMenu()
{
isMenuOpen = false;
isMobileMenuOpen = false;
StateHasChanged();
}
/// <summary>
/// Dispose method.
/// </summary>
public void Dispose()
{
NavigationManager.LocationChanged -= LocationChanged;
}
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
@@ -113,7 +130,7 @@
}
}
void LocationChanged(object? sender, LocationChangedEventArgs e)
private void LocationChanged(object? sender, LocationChangedEventArgs e)
{
isMenuOpen = false;
isMobileMenuOpen = false;
@@ -129,17 +146,4 @@
{
isMobileMenuOpen = !isMobileMenuOpen;
}
[JSInvokable]
public void CloseMenu()
{
isMenuOpen = false;
isMobileMenuOpen = false;
StateHasChanged();
}
public void Dispose()
{
NavigationManager.LocationChanged -= LocationChanged;
}
}

View File

@@ -10,6 +10,7 @@ namespace AliasVault.WebApp.Pages.Base;
using AliasVault.WebApp.Auth.Services;
using AliasVault.WebApp.Components.Models;
using AliasVault.WebApp.Services;
using AliasVault.WebApp.Services.Database;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
@@ -49,10 +50,10 @@ public class PageBase : OwningComponentBase
public IJSRuntime Js { get; set; } = null!;
/// <summary>
/// Gets or sets the AliasClientDbService.
/// Gets or sets the DbService.
/// </summary>
[Inject]
public AliasClientDbService AliasClientDbService { get; set; } = null!;
public DbService DbService { get; set; } = null!;
/// <summary>
/// Gets or sets the AuthService.

View File

@@ -22,14 +22,12 @@
}
<button class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800" @onclick="AddRecord">Add new record</button>
<button class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800" @onclick="LoadAliasFromDbAsync">Load from db again</button>
<button class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800" @onclick="ExportDbToString">Save DB to remote</button>
<button class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800" @onclick="SaveDb">SaveChanges</button>
<div class="grid gap-4 px-4 mb-4 md:grid-cols-4 xl:grid-cols-6">
@foreach (var password in Passwords)
{
<div>
<button @onclick="() => RemovePassword(password)">Remove</button>
<p>@password.CreatedAt</p>
<p>@password.Value</p>
</div>
@@ -41,6 +39,14 @@
private bool IsLoading { get; set; } = true;
private List<Password> Passwords { get; set; } = new();
private async Task RemovePassword(Password password)
{
var db = await DbService.GetDbContextAsync();
db.Passwords.Remove(password);
await DbService.SaveDatabaseAsync();
await RefreshAliasFromDbAsync();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -48,22 +54,20 @@
if (firstRender)
{
await LoadAliasFromDbAsync();
await RefreshAliasFromDbAsync();
}
}
private async Task LoadAliasFromDbAsync()
private async Task RefreshAliasFromDbAsync()
{
IsLoading = true;
StateHasChanged();
var db = await AliasClientDbService.GetDbContextAsync();
var db = await DbService.GetDbContextAsync();
// Load all passwords from the database
var passwords = await db.Passwords.ToListAsync();
// Show them on page
Passwords = passwords;
IsLoading = false;
StateHasChanged();
@@ -71,24 +75,13 @@
private async Task AddRecord()
{
var db = await AliasClientDbService.GetDbContextAsync();
var db = await DbService.GetDbContextAsync();
// Insert row in database
db.Passwords.Add(new Password() { Id = Guid.NewGuid(), Value = "Test factory insert SQLite", CreatedAt = DateTime.Now });
// Save changes
await db.SaveChangesAsync();
await LoadAliasFromDbAsync();
}
private async Task ExportDbToString()
{
await AliasClientDbService.SaveDatabaseAsync();
}
private async Task SaveDb()
{
await (await AliasClientDbService.GetDbContextAsync()).SaveChangesAsync();
// Save changes and upload db to remote.
await DbService.SaveDatabaseAsync();
await RefreshAliasFromDbAsync();
}
}

View File

@@ -9,6 +9,7 @@ using AliasVault.WebApp;
using AliasVault.WebApp.Auth.Providers;
using AliasVault.WebApp.Auth.Services;
using AliasVault.WebApp.Services;
using AliasVault.WebApp.Services.Database;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
@@ -36,7 +37,7 @@ builder.Services.AddTransient<AliasVaultApiHandlerService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
builder.Services.AddScoped<AliasService>();
builder.Services.AddScoped<AliasClientDbService>();
builder.Services.AddScoped<DbService>();
builder.Services.AddScoped<GlobalNotificationService>();
builder.Services.AddSingleton<ClipboardCopyService>();

View File

@@ -1,18 +1,17 @@
//-----------------------------------------------------------------------
// <copyright file="AliasClientDbService.cs" company="lanedirt">
// <copyright file="DbService.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.WebApp.Services;
namespace AliasVault.WebApp.Services.Database;
using System.Data;
using System.Net.Http.Json;
using AliasClientDb;
using AliasVault.Shared.Models.WebApi;
using AliasVault.WebApp.Auth.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.JSInterop;
@@ -22,32 +21,45 @@ using Microsoft.JSInterop;
/// with a AliasClientDb database instance that is only persisted in memory due to the encryption requirements of the
/// database itself. The database should not be persisted to disk when in un-encrypted form.
/// </summary>
public class AliasClientDbService
public class DbService
{
private readonly AuthService _authService;
private readonly IJSRuntime _jsRuntime;
private readonly HttpClient _httpClient;
private readonly DbServiceState _state = new();
private AliasClientDbContext? _dbContext;
private Task _initializationTask;
private bool _isSuccessfullyInitialized;
private int _retryCount;
/// <summary>
/// Initializes a new instance of the <see cref="AliasClientDbService"/> class.
/// Initializes a new instance of the <see cref="DbService"/> class.
/// </summary>
/// <param name="authService">AuthService.</param>
/// <param name="jsRuntime">IJSRuntime.</param>
/// <param name="httpClient">HttpClient.</param>
public AliasClientDbService(AuthService authService, IJSRuntime jsRuntime, HttpClient httpClient)
public DbService(AuthService authService, IJSRuntime jsRuntime, HttpClient httpClient)
{
_authService = authService;
_jsRuntime = jsRuntime;
_httpClient = httpClient;
// Set the initial state of the database service.
_state.UpdateState(DbServiceState.DatabaseStatus.Idle);
// Initialize the database asynchronously
_initializationTask = InitializeDatabaseAsync();
}
/// <summary>
/// Gets database service state object which can be subscribed to.
/// </summary>
/// <returns>DbServiceState instance.</returns>
public DbServiceState GetState()
{
return _state;
}
/// <summary>
/// Ensures that the service initialization is complete before proceeding.
/// </summary>
@@ -91,46 +103,58 @@ public class AliasClientDbService
{
await EnsureInitializedAsync();
using var memoryStream = new MemoryStream();
using var connection = new SqliteConnection(_dbContext!.Database.GetDbConnection().ConnectionString);
await connection.OpenAsync();
using var command = connection.CreateCommand();
command.CommandText = "VACUUM main INTO @fileName";
// Set the initial state of the database service.
_state.UpdateState(DbServiceState.DatabaseStatus.Saving);
var tempFileName = Path.GetRandomFileName();
command.Parameters.Add(new SqliteParameter("@fileName", tempFileName));
await command.ExecuteNonQueryAsync();
// Save the actual dbContext.
await _dbContext!.SaveChangesAsync();
var bytes = await File.ReadAllBytesAsync(tempFileName);
string base64String = await ExportSqliteToBase64Async();
string base64String = Convert.ToBase64String(bytes);
File.Delete(tempFileName);
// Encrypt base64 string.
// Encrypt using IJSInterop
Console.WriteLine("Encrypted using key: " + _authService.GetEncryptionKeyAsBase64Async());
// Encrypt base64 string using IJSInterop.
string encryptedBase64String = await _jsRuntime.InvokeAsync<string>("cryptoInterop.encrypt", base64String, _authService.GetEncryptionKeyAsBase64Async());
// Decrypt it again
string decryptedBase64String = await _jsRuntime.InvokeAsync<string>("cryptoInterop.decrypt", encryptedBase64String, _authService.GetEncryptionKeyAsBase64Async());
// Print original, encrypted and decrypted sting to console
Console.WriteLine("Original: " + base64String);
Console.WriteLine("Encrypted: " + encryptedBase64String);
Console.WriteLine("Decrypted: " + decryptedBase64String);
// Save to webapi.
var success = await SaveToServerAsync(encryptedBase64String);
if (success)
{
Console.WriteLine("Database succesfully saved to server.");
_state.UpdateState(DbServiceState.DatabaseStatus.Idle);
}
else
{
Console.WriteLine("Failed to save database to server.");
_state.UpdateState(DbServiceState.DatabaseStatus.Error);
}
}
/// <summary>
/// Export the in-memory SQLite database to a base64 string.
/// </summary>
/// <returns>Base64 encoded string that represents SQLite database.</returns>
public async Task<string> ExportSqliteToBase64Async()
{
var tempFileName = Path.GetRandomFileName();
// Export SQLite memory database to a temp file.
using var memoryStream = new MemoryStream();
using var connection = new SqliteConnection(_dbContext!.Database.GetDbConnection().ConnectionString);
await connection.OpenAsync();
using var command = connection.CreateCommand();
command.CommandText = "VACUUM main INTO @fileName";
command.Parameters.Add(new SqliteParameter("@fileName", tempFileName));
await command.ExecuteNonQueryAsync();
// Get bytes.
var bytes = await File.ReadAllBytesAsync(tempFileName);
string base64String = Convert.ToBase64String(bytes);
// Delete temp file.
File.Delete(tempFileName);
return base64String;
}
private static async Task ImportDbContextFromBase64Async(AliasClientDbContext dbContext, string base64String)
{
var bytes = Convert.FromBase64String(base64String);
@@ -202,6 +226,8 @@ public class AliasClientDbService
return;
}
_state.UpdateState(DbServiceState.DatabaseStatus.Loading);
// Create a new in-memory database.
string connectionString = "Data Source=AliasClientDb.sqlite";
using var connection = new SqliteConnection(connectionString);
@@ -221,10 +247,12 @@ public class AliasClientDbService
if (loaded)
{
_isSuccessfullyInitialized = true;
_state.UpdateState(DbServiceState.DatabaseStatus.Idle);
Console.WriteLine("Database succesfully loaded from server.");
}
else
{
_state.UpdateState(DbServiceState.DatabaseStatus.Error);
Console.WriteLine("Failed to load database from server.");
}
}
@@ -284,7 +312,7 @@ public class AliasClientDbService
try
{
await _httpClient.PostAsJsonAsync<Vault>("api/v1/Vault", vaultObject);
await _httpClient.PostAsJsonAsync("api/v1/Vault", vaultObject);
return true;
}
catch

View File

@@ -0,0 +1,122 @@
//-----------------------------------------------------------------------
// <copyright file="DbServiceState.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.WebApp.Services.Database;
/// <summary>
/// Class to manage the state of the AliasClientDbService that others can subscribe to events for.
/// </summary>
public class DbServiceState
{
private DatabaseState _currentState = new();
/// <summary>
/// Subscribe to this event to get notified when the state of the database changes.
/// </summary>
public event EventHandler<DatabaseState> StateChanged = (sender, e) => { };
/// <summary>
/// Database status enum.
/// </summary>
public enum DatabaseStatus
{
/// <summary>
/// No database operation is in progress.
/// </summary>
Idle,
/// <summary>
/// Database is loading from server.
/// </summary>
Loading,
/// <summary>
/// Database is saving to server.
/// </summary>
Saving,
/// <summary>
/// An error occurred during a database operation.
/// </summary>
Error,
}
/// <summary>
/// Gets the current state of the database.
/// </summary>
public DatabaseState CurrentState
{
get => _currentState;
private set
{
if (_currentState != value)
{
_currentState = value;
OnStateChanged(_currentState);
}
}
}
/// <summary>
/// Update the state of the database.
/// </summary>
/// <param name="status">New status.</param>
public void UpdateState(DatabaseStatus status)
{
CurrentState = new DatabaseState
{
Status = status,
Message = string.Empty,
LastUpdated = DateTime.Now,
};
}
/// <summary>
/// Update the state of the database with an additional message.
/// </summary>
/// <param name="status">New status.</param>
/// <param name="message">Status message.</param>
public void UpdateState(DatabaseStatus status, string message)
{
CurrentState = new DatabaseState
{
Status = status,
Message = message,
LastUpdated = DateTime.Now,
};
}
/// <summary>
/// OnStateChanged event handler.
/// </summary>
/// <param name="newState">The new state.</param>
protected virtual void OnStateChanged(DatabaseState newState)
{
StateChanged?.Invoke(this, newState);
}
/// <summary>
/// Database state class.
/// </summary>
public class DatabaseState
{
/// <summary>
/// Gets or sets the current status of the database.
/// </summary>
public DatabaseStatus Status { get; set; } = DatabaseStatus.Idle;
/// <summary>
/// Gets or sets the message associated with the current status.
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the last time the state was updated.
/// </summary>
public DateTime LastUpdated { get; set; } = DateTime.Now;
}
}

View File

@@ -17,6 +17,7 @@
@using AliasVault.WebApp.Components.Loading
@using AliasVault.WebApp.Pages.Base
@using AliasVault.WebApp.Services
@using AliasVault.WebApp.Services.Database
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Blazored.LocalStorage

View File

@@ -607,10 +607,6 @@ video {
top: 2.5rem;
}
.bottom-0 {
bottom: 0px;
}
.z-10 {
z-index: 10;
}
@@ -635,10 +631,6 @@ video {
grid-column: 1 / -1;
}
.m-4 {
margin: 1rem;
}
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
@@ -690,6 +682,10 @@ video {
margin-left: 0.25rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.ml-3 {
margin-left: 0.75rem;
}
@@ -722,6 +718,10 @@ video {
margin-top: 0px;
}
.mt-2 {
margin-top: 0.5rem;
}
.mt-4 {
margin-top: 1rem;
}
@@ -734,10 +734,6 @@ video {
margin-top: 2rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.block {
display: block;
}
@@ -802,12 +798,8 @@ video {
height: 100%;
}
.h-24 {
height: 6rem;
}
.min-h-screen {
min-height: 100vh;
.h-7 {
height: 1.75rem;
}
.w-1\/2 {
@@ -850,8 +842,8 @@ video {
width: 100%;
}
.w-24 {
width: 6rem;
.w-7 {
width: 1.75rem;
}
.min-w-full {
@@ -932,6 +924,10 @@ video {
justify-content: flex-start;
}
.justify-end {
justify-content: flex-end;
}
.justify-center {
justify-content: center;
}
@@ -1139,16 +1135,6 @@ video {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.bg-red-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
}
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
@@ -1157,6 +1143,10 @@ video {
fill: #d68338;
}
.fill-none {
fill: none;
}
.p-2 {
padding: 0.5rem;
}
@@ -1173,6 +1163,11 @@ video {
padding: 1.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
@@ -1279,11 +1274,6 @@ video {
line-height: 1rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-bold {
font-weight: 700;
}
@@ -1375,20 +1365,6 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity));
}
.text-red-700 {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0;
}
@@ -1625,11 +1601,6 @@ video {
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.dark\:bg-red-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
@@ -1679,16 +1650,6 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:text-gray-700:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.dark\:text-red-300:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(252 165 165 / var(--tw-text-opacity));
}
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));

View File

@@ -14,8 +14,6 @@ namespace AliasVault.E2ETests.Tests;
[TestFixture]
public class UnlockTests : PlaywrightTest
{
private static readonly Random Random = new();
/// <summary>
/// Test that the unlock page is displayed after hard refresh which should
/// clear the encryption key from memory.