Extracts inline styles from Razor pages into modular CSS files Adds new CSS files for components and specific page styles Improves code organization and maintainability by separating styling concerns Updates layout to include new CSS files and optional style sections
175 lines
6.4 KiB
Text
175 lines
6.4 KiB
Text
@page
|
|
@model PlanTempusAdmin.Pages.Azure.ContainerModel
|
|
@{
|
|
ViewData["Title"] = $"Container: {Model.Name}";
|
|
}
|
|
|
|
@section Styles {
|
|
<link rel="stylesheet" href="~/css/pages/azure.css" asp-append-version="true" />
|
|
}
|
|
|
|
<div class="page-header">
|
|
<div class="breadcrumb">
|
|
<a href="/Azure">Azure Storage</a>
|
|
<span class="separator">/</span>
|
|
@if (string.IsNullOrEmpty(Model.Prefix))
|
|
{
|
|
<span>@Model.Name</span>
|
|
}
|
|
else
|
|
{
|
|
<a href="/Azure/Container?name=@Model.Name">@Model.Name</a>
|
|
var parts = Model.Prefix.Split('/').Where(p => !string.IsNullOrEmpty(p)).ToList();
|
|
var currentPath = "";
|
|
foreach (var part in parts)
|
|
{
|
|
currentPath += part + "/";
|
|
<span class="separator">/</span>
|
|
if (currentPath == Model.Prefix + "/")
|
|
{
|
|
<span>@part</span>
|
|
}
|
|
else
|
|
{
|
|
<a href="/Azure/Container?name=@Model.Name&prefix=@currentPath">@part</a>
|
|
}
|
|
}
|
|
}
|
|
</div>
|
|
<h1 class="page-title">@Model.Name</h1>
|
|
<p class="page-subtitle">@Model.Details.BlobCount filer · @FormatBytes(Model.Details.TotalSize)</p>
|
|
</div>
|
|
|
|
@if (!Model.IsConnected)
|
|
{
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p class="text-danger">Kan ikke forbinde til Azure Storage</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<!-- Directories -->
|
|
@if (Model.Details.Prefixes.Count > 0)
|
|
{
|
|
<div class="card">
|
|
<div class="card-header">Mapper</div>
|
|
<div class="folder-grid">
|
|
@foreach (var prefix in Model.Details.Prefixes)
|
|
{
|
|
var folderName = prefix.Split('/').Last(p => !string.IsNullOrEmpty(p));
|
|
<a href="/Azure/Container?name=@Model.Name&prefix=@(prefix)/" class="folder-item">
|
|
<span class="folder-icon">📁</span>
|
|
<span class="folder-name">@folderName</span>
|
|
</a>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Files -->
|
|
<div class="card mt-2">
|
|
<div class="card-header">
|
|
Filer
|
|
<span class="header-meta">@Model.Details.Blobs.Count filer</span>
|
|
</div>
|
|
@if (Model.Details.Blobs.Count == 0)
|
|
{
|
|
<div class="card-body">
|
|
<p class="text-muted">Ingen filer i denne mappe</p>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Navn</th>
|
|
<th>Størrelse</th>
|
|
<th>Type</th>
|
|
<th>Tier</th>
|
|
<th>Ændret</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var blob in Model.Details.Blobs)
|
|
{
|
|
<tr>
|
|
<td>
|
|
<span class="file-icon">@GetFileIcon(blob.Name)</span>
|
|
<code class="blob-name" title="@blob.Name">@blob.FileName</code>
|
|
</td>
|
|
<td>@FormatBytes(blob.Size)</td>
|
|
<td><span class="badge">@(blob.ContentType ?? "-")</span></td>
|
|
<td>
|
|
@if (!string.IsNullOrEmpty(blob.AccessTier))
|
|
{
|
|
<span class="badge badge-tier @GetTierClass(blob.AccessTier)">@blob.AccessTier</span>
|
|
}
|
|
</td>
|
|
<td>@FormatTimeAgo(blob.LastModified)</td>
|
|
<td class="actions">
|
|
<a href="/Azure/Container?name=@Model.Name&blob=@blob.Name&handler=Download"
|
|
class="btn btn-sm" title="Download">⬇️</a>
|
|
<form method="post" asp-page-handler="Delete" style="display:inline;">
|
|
<input type="hidden" name="container" value="@Model.Name" />
|
|
<input type="hidden" name="blob" value="@blob.Name" />
|
|
<button type="submit" class="btn btn-sm btn-danger"
|
|
onclick="return confirm('Slet @blob.FileName?')" title="Slet">🗑️</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
}
|
|
</div>
|
|
|
|
<div class="mt-2">
|
|
<a href="/Azure" class="btn">← Tilbage til oversigt</a>
|
|
</div>
|
|
}
|
|
|
|
@functions {
|
|
string FormatBytes(long bytes)
|
|
{
|
|
if (bytes == 0) return "0 B";
|
|
var sizes = new[] { "B", "KB", "MB", "GB", "TB" };
|
|
var i = (int)Math.Floor(Math.Log(bytes) / Math.Log(1024));
|
|
return $"{Math.Round(bytes / Math.Pow(1024, i), 1)} {sizes[i]}";
|
|
}
|
|
|
|
string FormatTimeAgo(DateTimeOffset? time)
|
|
{
|
|
if (!time.HasValue) return "-";
|
|
var diff = DateTimeOffset.Now - time.Value;
|
|
if (diff.TotalMinutes < 1) return "lige nu";
|
|
if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m siden";
|
|
if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}t siden";
|
|
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d siden";
|
|
return time.Value.ToString("dd/MM HH:mm");
|
|
}
|
|
|
|
string GetFileIcon(string name)
|
|
{
|
|
if (name.EndsWith(".tar.gz")) return "📦";
|
|
if (name.EndsWith(".gz") || name.EndsWith(".zip") || name.EndsWith(".7z")) return "📦";
|
|
if (name.EndsWith(".sql")) return "🐘";
|
|
if (name.EndsWith(".bak")) return "💾";
|
|
if (name.EndsWith(".log")) return "📜";
|
|
if (name.EndsWith(".json")) return "📋";
|
|
if (name.EndsWith(".xml")) return "📄";
|
|
return "📄";
|
|
}
|
|
|
|
string GetTierClass(string tier)
|
|
{
|
|
var t = tier.ToLower();
|
|
if (t == "hot") return "hot";
|
|
if (t == "cool") return "cool";
|
|
if (t == "archive") return "archive";
|
|
return "";
|
|
}
|
|
}
|