mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-09 07:46:13 -04:00
Add vault storage insights page to web app (#1046)
This commit is contained in:
@@ -69,6 +69,11 @@
|
||||
@Localizer["SecuritySettingsNav"]
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink href="/settings/storage-insights" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
@Localizer["StorageInsightsNav"]
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink href="/settings/import-export" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
@Localizer["ImportExportNav"]
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
@page "/settings/storage-insights"
|
||||
@inherits MainBase
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Microsoft.Extensions.Localization
|
||||
|
||||
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="@Localizer["PageTitle"]"
|
||||
Description="@Localizer["PageDescription"]">
|
||||
</PageHeader>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<p class="text-gray-600 dark:text-gray-400">@SharedLocalizer["Loading"]</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-2 text-lg font-medium text-gray-900 dark:text-white">@Localizer["EstimatedTotalTitle"]</h3>
|
||||
<p class="text-4xl font-semibold text-gray-900 dark:text-white">@FormatSize(_estimatedTotalBytes)</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">@Localizer["EstimatedTotalDescription"]</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">@Localizer["CountsTitle"]</h3>
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div class="p-4 border border-gray-200 rounded-lg dark:border-gray-700">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">@Localizer["ItemCountLabel"]</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">@_totalItems</p>
|
||||
</div>
|
||||
<div class="p-4 border border-gray-200 rounded-lg dark:border-gray-700">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">@Localizer["ItemsWithAttachmentsLabel"]</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">@_itemsWithAttachments</p>
|
||||
</div>
|
||||
<div class="p-4 border border-gray-200 rounded-lg dark:border-gray-700">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">@Localizer["ItemsWithLogosLabel"]</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">@_itemsWithLogos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-2 text-lg font-medium text-gray-900 dark:text-white">@Localizer["BreakdownTitle"]</h3>
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">@Localizer["BreakdownDescription"]</p>
|
||||
|
||||
@if (_estimatedTotalBytes == 0)
|
||||
{
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">-</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex w-full h-4 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="bg-gray-400 dark:bg-gray-500" style="width: @(Percent(_baseOverheadBytes, _estimatedTotalBytes).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))%;" title="@Localizer["BreakdownBaseOverheadLabel"]"></div>
|
||||
<div class="bg-blue-500" style="width: @(Percent(_credentialBytes, _estimatedTotalBytes).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))%;" title="@Localizer["BreakdownCredentialsLabel"]"></div>
|
||||
<div class="bg-amber-500" style="width: @(Percent(_attachmentBytesWithOverhead, _estimatedTotalBytes).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))%;" title="@Localizer["BreakdownAttachmentsLabel"]"></div>
|
||||
<div class="bg-emerald-500" style="width: @(Percent(_logoBytesWithOverhead, _estimatedTotalBytes).ToString("F2", System.Globalization.CultureInfo.InvariantCulture))%;" title="@Localizer["BreakdownLogosLabel"]"></div>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-2 sm:grid-cols-2 lg:grid-cols-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-3 h-3 rounded-sm bg-gray-400 dark:bg-gray-500"></span>
|
||||
<span class="text-gray-700 dark:text-gray-300">@Localizer["BreakdownBaseOverheadLabel"]:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">@FormatSize(_baseOverheadBytes) (@Percent(_baseOverheadBytes, _estimatedTotalBytes).ToString("F1")%)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-3 h-3 rounded-sm bg-blue-500"></span>
|
||||
<span class="text-gray-700 dark:text-gray-300">@Localizer["BreakdownCredentialsLabel"]:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">@FormatSize(_credentialBytes) (@Percent(_credentialBytes, _estimatedTotalBytes).ToString("F1")%)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-3 h-3 rounded-sm bg-amber-500"></span>
|
||||
<span class="text-gray-700 dark:text-gray-300">@Localizer["BreakdownAttachmentsLabel"]:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">@FormatSize(_attachmentBytesWithOverhead) (@Percent(_attachmentBytesWithOverhead, _estimatedTotalBytes).ToString("F1")%)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-3 h-3 rounded-sm bg-emerald-500"></span>
|
||||
<span class="text-gray-700 dark:text-gray-300">@Localizer["BreakdownLogosLabel"]:</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">@FormatSize(_logoBytesWithOverhead) (@Percent(_logoBytesWithOverhead, _estimatedTotalBytes).ToString("F1")%)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-2 text-lg font-medium text-gray-900 dark:text-white">@Localizer["TopAttachmentsTitle"]</h3>
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">@Localizer["TopAttachmentsDescription"]</p>
|
||||
|
||||
@if (_topAttachments.Count == 0)
|
||||
{
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">-</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<SortableTable Columns="@_attachmentColumns" SortColumn="@string.Empty" SortDirection="SortDirection.Descending" OnSortChanged="(_ => Task.CompletedTask)">
|
||||
@foreach (var attachment in _topAttachments)
|
||||
{
|
||||
<SortableTableRow Class="cursor-pointer" OnClick="@(() => NavigateToItem(attachment.ItemId))">
|
||||
<SortableTableColumn IsPrimary="true">@attachment.Filename</SortableTableColumn>
|
||||
<SortableTableColumn>@FormatSize(attachment.SizeBytes)</SortableTableColumn>
|
||||
<SortableTableColumn>@(string.IsNullOrWhiteSpace(attachment.ItemName) ? "-" : attachment.ItemName)</SortableTableColumn>
|
||||
<SortableTableColumn>@attachment.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd")</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-2 text-lg font-medium text-gray-900 dark:text-white">@Localizer["TopLogosTitle"]</h3>
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">@Localizer["TopLogosDescription"]</p>
|
||||
|
||||
@if (_topLogos.Count == 0)
|
||||
{
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">-</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<SortableTable Columns="@_logoColumns" SortColumn="@string.Empty" SortDirection="SortDirection.Descending" OnSortChanged="(_ => Task.CompletedTask)">
|
||||
@foreach (var logo in _topLogos)
|
||||
{
|
||||
<SortableTableRow Class="@(logo.FirstItemId.HasValue ? "cursor-pointer" : string.Empty)" OnClick="@(logo.FirstItemId.HasValue ? EventCallback.Factory.Create(this, () => NavigateToItem(logo.FirstItemId!.Value)) : default)">
|
||||
<SortableTableColumn IsPrimary="true">@logo.Source</SortableTableColumn>
|
||||
<SortableTableColumn>@FormatSize(logo.SizeBytes)</SortableTableColumn>
|
||||
<SortableTableColumn>@(string.IsNullOrWhiteSpace(logo.MimeType) ? "-" : logo.MimeType)</SortableTableColumn>
|
||||
<SortableTableColumn>@logo.ItemCount @Localizer["ItemsUsingLogoSuffix"]</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
// Approximate per-row SQLite overhead in bytes. SQLite uses 4 KiB pages by default;
|
||||
// small rows pack many per page, while large blobs spill into overflow pages. These
|
||||
// constants cover row headers and the indexed columns AliasClientDbContext defines.
|
||||
// They are intentionally rough — attachments and logos dominate real vault size.
|
||||
private const int PerItemOverheadBytes = 200;
|
||||
private const int PerFieldValueOverheadBytes = 80;
|
||||
private const int PerAttachmentOverheadBytes = 120;
|
||||
private const int PerLogoOverheadBytes = 150;
|
||||
private const int PerTotpOverheadBytes = 100;
|
||||
private const int PerPasskeyOverheadBytes = 200;
|
||||
|
||||
// Empty AliasVault SQLite file is ~200 KB on disk: file header, page allocation
|
||||
// for each table/index, sqlite_master schema rows, and __EFMigrationsHistory.
|
||||
// Constant rather than measured because we don't have access to the file size
|
||||
// here and the value is stable across vaults of the same schema version.
|
||||
private const int BaseSqliteOverheadBytes = 200 * 1024;
|
||||
|
||||
private readonly List<TableColumn> _attachmentColumns =
|
||||
[
|
||||
new TableColumn { Title = "Filename", Sortable = false },
|
||||
new TableColumn { Title = "Size", Sortable = false },
|
||||
new TableColumn { Title = "Item", Sortable = false },
|
||||
new TableColumn { Title = "Created", Sortable = false },
|
||||
];
|
||||
|
||||
private readonly List<TableColumn> _logoColumns =
|
||||
[
|
||||
new TableColumn { Title = "Source", Sortable = false },
|
||||
new TableColumn { Title = "Size", Sortable = false },
|
||||
new TableColumn { Title = "Type", Sortable = false },
|
||||
new TableColumn { Title = "Used by", Sortable = false },
|
||||
];
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
|
||||
private int _totalItems;
|
||||
private int _itemsWithAttachments;
|
||||
private int _itemsWithLogos;
|
||||
private long _credentialBytes;
|
||||
private long _attachmentBytesWithOverhead;
|
||||
private long _logoBytesWithOverhead;
|
||||
private long _baseOverheadBytes;
|
||||
private long _estimatedTotalBytes;
|
||||
private List<AttachmentRow> _topAttachments = [];
|
||||
private List<LogoRow> _topLogos = [];
|
||||
|
||||
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Settings.StorageInsights", "AliasVault.Client");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbTitle"] });
|
||||
|
||||
// Localize the table column headers (created at field init time before localizer was available).
|
||||
_attachmentColumns[0].Title = Localizer["ColumnFilename"];
|
||||
_attachmentColumns[1].Title = Localizer["ColumnSize"];
|
||||
_attachmentColumns[2].Title = Localizer["ColumnItem"];
|
||||
_attachmentColumns[3].Title = Localizer["ColumnCreated"];
|
||||
_logoColumns[0].Title = Localizer["ColumnWebsiteURL"];
|
||||
_logoColumns[1].Title = Localizer["ColumnSize"];
|
||||
_logoColumns[2].Title = Localizer["ColumnMimeType"];
|
||||
_logoColumns[3].Title = Localizer["ColumnItemCount"];
|
||||
|
||||
await LoadStatisticsAsync();
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes)
|
||||
{
|
||||
const double Kib = 1024d;
|
||||
const double Mib = Kib * 1024d;
|
||||
|
||||
if (bytes < Kib)
|
||||
{
|
||||
return string.Create(System.Globalization.CultureInfo.CurrentCulture, $"{bytes} B");
|
||||
}
|
||||
|
||||
if (bytes < Mib)
|
||||
{
|
||||
return string.Create(System.Globalization.CultureInfo.CurrentCulture, $"{(bytes / Kib):F1} KB");
|
||||
}
|
||||
|
||||
return string.Create(System.Globalization.CultureInfo.CurrentCulture, $"{(bytes / Mib):F1} MB");
|
||||
}
|
||||
|
||||
private static double Percent(long part, long total)
|
||||
{
|
||||
return total == 0 ? 0d : Math.Round((double)part / total * 100d, 2);
|
||||
}
|
||||
|
||||
private async Task LoadStatisticsAsync()
|
||||
{
|
||||
var ctx = await DbService.GetDbContextAsync();
|
||||
|
||||
_totalItems = await ctx.Items.CountAsync(i => !i.IsDeleted && i.DeletedAt == null);
|
||||
_itemsWithAttachments = await ctx.Attachments
|
||||
.Where(a => !a.IsDeleted)
|
||||
.Select(a => a.ItemId)
|
||||
.Distinct()
|
||||
.CountAsync();
|
||||
_itemsWithLogos = await ctx.Items.CountAsync(i => !i.IsDeleted && i.DeletedAt == null && i.LogoId != null);
|
||||
|
||||
var attachmentRows = await ctx.Attachments
|
||||
.Where(a => !a.IsDeleted)
|
||||
.Select(a => new AttachmentRow
|
||||
{
|
||||
Id = a.Id,
|
||||
Filename = a.Filename,
|
||||
SizeBytes = a.Blob.Length,
|
||||
ItemId = a.ItemId,
|
||||
ItemName = a.Item.Name,
|
||||
CreatedAt = a.CreatedAt,
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var logoProjections = await ctx.Logos
|
||||
.Where(l => !l.IsDeleted && l.FileData != null)
|
||||
.Select(l => new
|
||||
{
|
||||
l.Id,
|
||||
l.Source,
|
||||
l.MimeType,
|
||||
SizeBytes = l.FileData!.Length,
|
||||
ItemIds = l.Items
|
||||
.Where(i => !i.IsDeleted && i.DeletedAt == null)
|
||||
.Select(i => i.Id)
|
||||
.ToList(),
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var logoRows = logoProjections
|
||||
.Select(l => new LogoRow
|
||||
{
|
||||
Id = l.Id,
|
||||
Source = l.Source,
|
||||
MimeType = l.MimeType,
|
||||
SizeBytes = l.SizeBytes,
|
||||
ItemCount = l.ItemIds.Count,
|
||||
FirstItemId = l.ItemIds.Count > 0 ? l.ItemIds[0] : null,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var fieldValueLengths = await ctx.FieldValues
|
||||
.Where(fv => !fv.IsDeleted)
|
||||
.Select(fv => fv.Value == null ? 0 : fv.Value.Length)
|
||||
.ToListAsync();
|
||||
var fieldValueBytes = fieldValueLengths.Sum(x => (long)x);
|
||||
var fieldValueCount = fieldValueLengths.Count;
|
||||
|
||||
var passkeySizes = await ctx.Passkeys
|
||||
.Where(p => !p.IsDeleted)
|
||||
.Select(p => new
|
||||
{
|
||||
UserHandleLen = p.UserHandle.Length,
|
||||
PrfKeyLen = p.PrfKey == null ? 0 : p.PrfKey.Length,
|
||||
AdditionalDataLen = p.AdditionalData == null ? 0 : p.AdditionalData.Length,
|
||||
PublicKeyLen = p.PublicKey.Length,
|
||||
PrivateKeyLen = p.PrivateKey.Length,
|
||||
})
|
||||
.ToListAsync();
|
||||
var passkeyBytes = passkeySizes.Sum(p => (long)(p.UserHandleLen + p.PrfKeyLen + p.AdditionalDataLen + p.PublicKeyLen + p.PrivateKeyLen));
|
||||
var passkeyCount = passkeySizes.Count;
|
||||
|
||||
var totpCount = await ctx.TotpCodes.CountAsync(t => !t.IsDeleted);
|
||||
|
||||
var attachmentBytes = attachmentRows.Sum(a => (long)a.SizeBytes);
|
||||
var logoBytes = logoRows.Sum(l => (long)l.SizeBytes);
|
||||
|
||||
_attachmentBytesWithOverhead = attachmentBytes + ((long)attachmentRows.Count * PerAttachmentOverheadBytes);
|
||||
_logoBytesWithOverhead = logoBytes + ((long)logoRows.Count * PerLogoOverheadBytes);
|
||||
|
||||
_credentialBytes = fieldValueBytes
|
||||
+ passkeyBytes
|
||||
+ ((long)_totalItems * PerItemOverheadBytes)
|
||||
+ ((long)fieldValueCount * PerFieldValueOverheadBytes)
|
||||
+ ((long)passkeyCount * PerPasskeyOverheadBytes)
|
||||
+ ((long)totpCount * PerTotpOverheadBytes);
|
||||
|
||||
_baseOverheadBytes = BaseSqliteOverheadBytes;
|
||||
|
||||
_estimatedTotalBytes = _credentialBytes
|
||||
+ _attachmentBytesWithOverhead
|
||||
+ _logoBytesWithOverhead
|
||||
+ _baseOverheadBytes;
|
||||
|
||||
_topAttachments = attachmentRows
|
||||
.OrderByDescending(a => a.SizeBytes)
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
_topLogos = logoRows
|
||||
.OrderByDescending(l => l.SizeBytes)
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void NavigateToItem(Guid itemId)
|
||||
{
|
||||
NavigationManager.NavigateTo($"/items/{itemId}");
|
||||
}
|
||||
|
||||
private sealed class AttachmentRow
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
|
||||
public string Filename { get; init; } = string.Empty;
|
||||
|
||||
public int SizeBytes { get; init; }
|
||||
|
||||
public Guid ItemId { get; init; }
|
||||
|
||||
public string? ItemName { get; init; }
|
||||
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
private sealed class LogoRow
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
|
||||
public string Source { get; init; } = string.Empty;
|
||||
|
||||
public string? MimeType { get; init; }
|
||||
|
||||
public int SizeBytes { get; init; }
|
||||
|
||||
public int ItemCount { get; init; }
|
||||
|
||||
public Guid? FirstItemId { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,10 @@
|
||||
<value>Security settings</value>
|
||||
<comment>Navigation link for security settings</comment>
|
||||
</data>
|
||||
<data name="StorageInsightsNav">
|
||||
<value>Storage insights</value>
|
||||
<comment>Navigation link for vault storage insights page</comment>
|
||||
</data>
|
||||
<data name="ImportExportNav">
|
||||
<value>Import / Export</value>
|
||||
<comment>Navigation link for import/export settings</comment>
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
|
||||
<!-- Page title and description -->
|
||||
<data name="PageTitle">
|
||||
<value>Vault storage insights</value>
|
||||
<comment>Page title for vault storage insights</comment>
|
||||
</data>
|
||||
<data name="PageDescription">
|
||||
<value>See an estimate of your vault size. For best sync performance, it's advised to keep your vault size small and delete items you no longer need.</value>
|
||||
<comment>Page description for vault storage insights</comment>
|
||||
</data>
|
||||
<data name="BreadcrumbTitle">
|
||||
<value>Storage insights</value>
|
||||
<comment>Breadcrumb title for storage insights page</comment>
|
||||
</data>
|
||||
|
||||
<!-- Estimated total -->
|
||||
<data name="EstimatedTotalTitle">
|
||||
<value>Estimated vault size</value>
|
||||
<comment>Title for the estimated total vault size card</comment>
|
||||
</data>
|
||||
<data name="EstimatedTotalDescription">
|
||||
<value>This is an approximation calculated from your local data.</value>
|
||||
<comment>Subtext explaining that the estimate is approximate</comment>
|
||||
</data>
|
||||
|
||||
<!-- Counts -->
|
||||
<data name="CountsTitle">
|
||||
<value>Overview</value>
|
||||
<comment>Title for the counts/overview section</comment>
|
||||
</data>
|
||||
<data name="ItemCountLabel">
|
||||
<value>Items</value>
|
||||
<comment>Label for total item count card</comment>
|
||||
</data>
|
||||
<data name="ItemsWithAttachmentsLabel">
|
||||
<value>Items with attachments</value>
|
||||
<comment>Label for count of items that have at least one attachment</comment>
|
||||
</data>
|
||||
<data name="ItemsWithLogosLabel">
|
||||
<value>Items with a logo</value>
|
||||
<comment>Label for count of items that reference a logo</comment>
|
||||
</data>
|
||||
|
||||
<!-- Breakdown -->
|
||||
<data name="BreakdownTitle">
|
||||
<value>Storage usage breakdown</value>
|
||||
<comment>Title for the breakdown bar section</comment>
|
||||
</data>
|
||||
<data name="BreakdownDescription">
|
||||
<value>Approximate share of vault size by category. Attachments and logos usually take up most of the space.</value>
|
||||
<comment>Description for the breakdown bar</comment>
|
||||
</data>
|
||||
<data name="BreakdownCredentialsLabel">
|
||||
<value>Credentials</value>
|
||||
<comment>Legend label for credential data slice</comment>
|
||||
</data>
|
||||
<data name="BreakdownAttachmentsLabel">
|
||||
<value>Attachments</value>
|
||||
<comment>Legend label for attachments slice</comment>
|
||||
</data>
|
||||
<data name="BreakdownLogosLabel">
|
||||
<value>Logos</value>
|
||||
<comment>Legend label for logos slice</comment>
|
||||
</data>
|
||||
<data name="BreakdownBaseOverheadLabel">
|
||||
<value>Database overhead</value>
|
||||
<comment>Legend label for the fixed SQLite base/schema overhead slice</comment>
|
||||
</data>
|
||||
|
||||
<!-- Top attachments table -->
|
||||
<data name="TopAttachmentsTitle">
|
||||
<value>Largest attachments</value>
|
||||
<comment>Title for the top attachments table</comment>
|
||||
</data>
|
||||
<data name="TopAttachmentsDescription">
|
||||
<value>Top 10 largest file attachments. Remove the attachment if you no longer need it.</value>
|
||||
<comment>Description for the top attachments table</comment>
|
||||
</data>
|
||||
|
||||
<!-- Top logos table -->
|
||||
<data name="TopLogosTitle">
|
||||
<value>Largest logos</value>
|
||||
<comment>Title for the top logos table</comment>
|
||||
</data>
|
||||
<data name="TopLogosDescription">
|
||||
<value>Top 10 largest service logos. Logos are reused for items with the same domain.</value>
|
||||
<comment>Description for the top logos table</comment>
|
||||
</data>
|
||||
|
||||
<!-- Table column headers -->
|
||||
<data name="ColumnFilename">
|
||||
<value>Filename</value>
|
||||
<comment>Table column header for attachment filename</comment>
|
||||
</data>
|
||||
<data name="ColumnSize">
|
||||
<value>Size</value>
|
||||
<comment>Table column header for size in KB</comment>
|
||||
</data>
|
||||
<data name="ColumnItem">
|
||||
<value>Item</value>
|
||||
<comment>Table column header for the parent item name</comment>
|
||||
</data>
|
||||
<data name="ColumnCreated">
|
||||
<value>Created</value>
|
||||
<comment>Table column header for creation date</comment>
|
||||
</data>
|
||||
<data name="ColumnWebsiteURL">
|
||||
<value>Website URL</value>
|
||||
<comment>Table column header for logo source domain</comment>
|
||||
</data>
|
||||
<data name="ColumnMimeType">
|
||||
<value>Type</value>
|
||||
<comment>Table column header for MIME type</comment>
|
||||
</data>
|
||||
<data name="ColumnItemCount">
|
||||
<value>Used by</value>
|
||||
<comment>Table column header for the number of items using a logo</comment>
|
||||
</data>
|
||||
|
||||
<!-- Misc -->
|
||||
<data name="ItemsUsingLogoSuffix">
|
||||
<value>items</value>
|
||||
<comment>Suffix shown after the count of items using a given logo (e.g. "3 items")</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1638,6 +1638,10 @@ video {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-sm {
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.rounded-b-lg {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
@@ -1809,6 +1813,11 @@ video {
|
||||
border-color: rgb(254 240 138 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-yellow-400 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(250 204 21 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 243 199 / var(--tw-bg-opacity));
|
||||
@@ -2018,6 +2027,21 @@ video {
|
||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-amber-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(245 158 11 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-emerald-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(16 185 129 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-yellow-200 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 240 138 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
@@ -3294,6 +3318,11 @@ video {
|
||||
border-color: rgb(133 77 14 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-yellow-600:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(202 138 4 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-amber-800\/30:is(.dark *) {
|
||||
background-color: rgb(146 64 14 / 0.3);
|
||||
}
|
||||
@@ -4269,6 +4298,10 @@ video {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.lg\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user