1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-04-24 07:15:47 +02:00

Redesign Web tables

This commit is contained in:
chylex 2023-12-19 06:50:17 +01:00
parent 578ec2d11c
commit e679b17e3b
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
16 changed files with 361 additions and 292 deletions

View File

@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis;
namespace Phantom.Utils.Collections;
public sealed class Table<TRow, TKey> : IReadOnlyList<TRow>, IReadOnlyDictionary<TKey, TRow> where TRow : notnull where TKey : notnull {
public sealed class TableData<TRow, TKey> : IReadOnlyList<TRow>, IReadOnlyDictionary<TKey, TRow> where TRow : notnull where TKey : notnull {
private readonly List<TRow> rowList = new();
private readonly Dictionary<TKey, TRow> rowDictionary = new ();

View File

@ -39,8 +39,10 @@ $dropdown-link-active-bg: mix($gray-200, $gray-300, 75%);
@import "./components";
.spinner-border-sm {
--bs-spinner-border-width: 0.15em;
.spinner-border {
--bs-spinner-width: 1em;
--bs-spinner-height: 1em;
--bs-spinner-border-width: 0.15rem;
}
.progress {

View File

@ -1,9 +1,9 @@
.progress {
height: 4px;
margin: 0.15rem 0;
}
.progress-label {
width: 100%;
margin-bottom: 0.15rem;
font-size: 0.9rem;
}

View File

@ -0,0 +1,14 @@
@using System.Globalization
<p>
<time datetime="@Time.ToString("o", CultureInfo.InvariantCulture)" data-time-type="relative">
@Time.ToString("dd MMM yyyy, HH:mm:ss", CultureInfo.InvariantCulture)
</time>
</p>
<small>@Time.ToString("zzz", CultureInfo.InvariantCulture)</small>
@code {
[Parameter, EditorRequired]
public DateTimeOffset Time { get; set; }
}

View File

@ -1,4 +1,4 @@
<th style="min-width: @minWidth; width: @preferredWidth;" class="@Class">
<th style="@style" class="@Class">
@ChildContent
</th>
@ -7,32 +7,29 @@
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter]
public string? MinWidth { get; set; }
[Parameter]
public string? Width { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
private string minWidth = string.Empty;
private string preferredWidth = string.Empty;
private string style = string.Empty;
protected override void OnParametersSet() {
if (string.IsNullOrEmpty(Width)) {
minWidth = string.Empty;
preferredWidth = string.Empty;
return;
List<string> styles = new (2);
if (MinWidth != null) {
styles.Add("min-width: " + MinWidth);
}
if (Width != null) {
styles.Add("width: " + Width);
}
int separator = Width.IndexOf(';');
if (separator == -1) {
minWidth = Width;
preferredWidth = Width;
return;
}
var span = Width.AsSpan();
minWidth = span[..separator].Trim().ToString();
preferredWidth = span[(separator + 1)..].Trim().ToString();
style = string.Join(';', styles);
}
}

View File

@ -0,0 +1,58 @@
@typeparam TItem
<div class="horizontal-scroll">
<table class="table align-middle@(Class.Length == 0 ? "" : " " + Class)">
<thead>
<tr>
@HeaderRow
</tr>
</thead>
@if (Items is null) {
<tbody>
<tr>
<td colspan="1000" class="fw-semibold">
Loading...
</td>
</tr>
</tbody>
}
else if (Items.Count > 0) {
<tbody>
@foreach (var item in Items) {
<tr>
@ItemRow(item)
</tr>
}
</tbody>
}
else if (NoItemsRow != null) {
<tfoot>
<tr>
<td colspan="1000">@NoItemsRow</td>
</tr>
</tfoot>
}
</table>
</div>
@code {
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter, EditorRequired]
public RenderFragment HeaderRow { get; set; } = null!;
[Parameter, EditorRequired]
public RenderFragment<TItem> ItemRow { get; set; } = null!;
[Parameter]
public RenderFragment? NoItemsRow { get; set; } = null!;
[Parameter, EditorRequired]
public IReadOnlyList<TItem>? Items { get; set; }
}

View File

@ -7,75 +7,63 @@
<h1>Agents</h1>
<table class="table align-middle">
<thead>
<tr>
<Column Width="200px; 44%">Name</Column>
<Column Width=" 90px; 19%" Class="text-end">Instances</Column>
<Column Width="145px; 21%" Class="text-end">Memory</Column>
<Column Width="180px; 8%">Version</Column>
<Column Width="320px">Identifier</Column>
<Column Width="100px; 8%" Class="text-center">Status</Column>
<Column Width="215px" Class="text-end">Last Ping</Column>
</tr>
</thead>
@if (!agentTable.IsEmpty) {
<tbody>
@foreach (var agent in agentTable) {
var usedInstances = agent.Stats?.RunningInstanceCount;
var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes;
<tr>
<td>@agent.Name</td>
<td class="text-end">
<ProgressBar Value="@(usedInstances ?? 0)" Maximum="@agent.MaxInstances">
@(usedInstances?.ToString() ?? "?") / @agent.MaxInstances
</ProgressBar>
</td>
<td class="text-end">
<ProgressBar Value="@(usedMemory ?? 0)" Maximum="@agent.MaxMemory.InMegabytes">
@(usedMemory?.ToString() ?? "?") / @agent.MaxMemory.InMegabytes MB
</ProgressBar>
</td>
<td class="text-condensed">
Build: <code>@agent.BuildVersion</code>
<br>
Protocol: <code>v@(agent.ProtocolVersion)</code>
</td>
<td>
<code class="text-uppercase">@agent.Guid.ToString()</code>
</td>
@if (agent.IsOnline) {
<td class="text-center text-success">Online</td>
<td class="text-end"></td>
}
else {
<td class="text-center text-danger">Offline</td>
@if (agent.LastPing is {} lastPing) {
<td class="text-end">
<time datetime="@lastPing.ToString("o")" data-time-type="relative">@lastPing.ToString()</time>
</td>
}
else {
<td class="text-end">-</td>
}
}
</tr>
<Table Items="agentTable">
<HeaderRow>
<Column Width="50%">Name</Column>
<Column Class="text-end" Width="24%" MinWidth="90px">Instances</Column>
<Column Class="text-end" Width="26%" MinWidth="145px">Memory</Column>
<Column>Version</Column>
<Column Class="text-center">Status</Column>
<Column Class="text-end" MinWidth="200px">Last Ping</Column>
</HeaderRow>
<ItemRow Context="agent">
@{
var usedInstances = agent.Stats?.RunningInstanceCount;
var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes;
}
<td>
<p class="fw-semibold">@agent.Name</p>
<small class="font-monospace text-uppercase">@agent.Guid.ToString()</small>
</td>
<td class="text-end">
<ProgressBar Value="@(usedInstances ?? 0)" Maximum="@agent.MaxInstances">
@(usedInstances?.ToString() ?? "?") / @agent.MaxInstances.ToString()
</ProgressBar>
</td>
<td class="text-end">
<ProgressBar Value="@(usedMemory ?? 0)" Maximum="@agent.MaxMemory.InMegabytes">
@(usedMemory?.ToString() ?? "?") / @agent.MaxMemory.InMegabytes.ToString() MB
</ProgressBar>
</td>
<td class="text-condensed">
Build: <span class="font-monospace">@agent.BuildVersion</span>
<br>
Protocol: <span class="font-monospace">v@(agent.ProtocolVersion.ToString())</span>
</td>
@if (agent.IsOnline) {
<td class="fw-semibold text-center text-success">Online</td>
<td class="text-end">-</td>
}
else {
<td class="fw-semibold text-center">Offline</td>
<td class="text-end">
@if (agent.LastPing is {} lastPing) {
<TimeWithOffset Time="lastPing" />
}
</tbody>
}
else {
<tfoot>
<tr>
<td colspan="7">No agents registered.</td>
</tr>
</tfoot>
}
</table>
else {
<text>N/A</text>
}
</td>
}
</ItemRow>
<NoItemsRow>
No agents registered.
</NoItemsRow>
</Table>
@code {
private readonly Table<AgentWithStats, Guid> agentTable = new();
private readonly TableData<AgentWithStats, Guid> agentTable = new();
protected override void OnInitialized() {
AgentManager.AgentsChanged.Subscribe(this, agents => {

View File

@ -12,48 +12,42 @@
<h1>Audit Log</h1>
<table class="table">
<thead>
<tr>
<Column Width="165px" Class="text-end">Time</Column>
<Column Width="320px; 20%">User</Column>
<Column Width="160px">Event Type</Column>
<Column Width="320px; 20%">Subject</Column>
<Column Width="100px; 60%">Data</Column>
</tr>
</thead>
<tbody>
@foreach (var logItem in logItems) {
DateTimeOffset time = logItem.UtcTime.ToLocalTime();
<tr>
<td class="text-end">
<time datetime="@time.ToString("o")">@time.ToString()</time>
</td>
<td>
@(logItem.UserName ?? "-")
<br>
<code class="text-uppercase">@logItem.UserGuid</code>
</td>
<td>@logItem.EventType.ToNiceString()</td>
<td>
@if (logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName) {
@subjectName
<br>
}
<code class="text-uppercase">@(logItem.SubjectId ?? "-")</code>
</td>
<td>
<code>@logItem.JsonData</code>
</td>
</tr>
}
</tbody>
</table>
<Table TItem="AuditLogItem" Items="logItems">
<HeaderRow>
<Column Class="text-end" MinWidth="200px">Time</Column>
<Column>User</Column>
<Column>Event Type</Column>
<Column>Subject</Column>
<Column Width="100%">Data</Column>
</HeaderRow>
<ItemRow Context="logItem">
<td class="text-end">
<TimeWithOffset Time="logItem.UtcTime.ToLocalTime()" />
</td>
<td>
<p class="fw-semibold">@(logItem.UserName ?? "-")</p>
<small class="font-monospace text-uppercase">@logItem.UserGuid.ToString()</small>
</td>
<td>
<p>@logItem.EventType.ToNiceString()</p>
</td>
<td>
<p class="fw-semibold">@(logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName ? subjectName : "-")</p>
<small class="font-monospace text-uppercase">@(logItem.SubjectId ?? "-")</small>
</td>
<td>
<code>@logItem.JsonData</code>
</td>
</ItemRow>
<NoItemsRow>
No audit log entries found.
</NoItemsRow>
</Table>
@code {
private CancellationTokenSource? initializationCancellationTokenSource;
private ImmutableArray<AuditLogItem> logItems = ImmutableArray<AuditLogItem>.Empty;
private ImmutableArray<AuditLogItem>? logItems;
private ImmutableDictionary<Guid, string>? userNamesByGuid;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
@ -72,9 +66,9 @@
private string? GetSubjectName(AuditLogSubjectType type, string id) {
return type switch {
AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
AuditLogSubjectType.User => userNamesByGuid != null && userNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
_ => null
AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
AuditLogSubjectType.User => userNamesByGuid != null && userNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
_ => null
};
}

View File

@ -13,53 +13,45 @@
<h1>Event Log</h1>
<table class="table">
<thead>
<tr>
<Column Width="165px" Class="text-end">Time</Column>
<Column Width="320px; 20%">Agent</Column>
<Column Width="160px">Event Type</Column>
<Column Width="320px; 20%">Subject</Column>
<Column Width="100px; 60%">Data</Column>
</tr>
</thead>
<tbody>
@foreach (var logItem in logItems) {
DateTimeOffset time = logItem.UtcTime.ToLocalTime();
<tr>
<td class="text-end">
<time datetime="@time.ToString("o")">@time.ToString()</time>
</td>
<td>
@if (logItem.AgentGuid is {} agentGuid) {
@(GetAgentName(agentGuid))
<br>
<code class="text-uppercase">@agentGuid</code>
}
else {
<text>-</text>
}
</td>
<td>@logItem.EventType.ToNiceString()</td>
<td>
@if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) {
@subjectName
<br>
}
<code class="text-uppercase">@logItem.SubjectId</code>
</td>
<td>
<code>@logItem.JsonData</code>
</td>
</tr>
}
</tbody>
</table>
<Table TItem="EventLogItem" Items="logItems">
<HeaderRow>
<Column Class="text-end" MinWidth="200px">Time</Column>
<Column>Agent</Column>
<Column>Event Type</Column>
<Column>Subject</Column>
<Column Width="100%">Data</Column>
</HeaderRow>
<ItemRow Context="logItem">
<td class="text-end">
<TimeWithOffset Time="logItem.UtcTime.ToLocalTime()" />
</td>
<td>
@if (logItem.AgentGuid is {} agentGuid) {
<p class="fw-semibold">@(GetAgentName(agentGuid))</p>
<small class="font-monospace text-uppercase">@agentGuid.ToString()</small>
}
else {
<text>-</text>
}
</td>
<td>@logItem.EventType.ToNiceString()</td>
<td>
<p class="fw-semibold">@(GetSubjectName(logItem.SubjectType, logItem.SubjectId) ?? "-")</p>
<small class="font-monospace text-uppercase">@(logItem.SubjectId)</small>
</td>
<td>
<code>@logItem.JsonData</code>
</td>
</ItemRow>
<NoItemsRow>
No event log entries found.
</NoItemsRow>
</Table>
@code {
private CancellationTokenSource? initializationCancellationTokenSource;
private ImmutableArray<EventLogItem> logItems = ImmutableArray<EventLogItem>.Empty;
private ImmutableArray<EventLogItem>? logItems;
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
@ -82,8 +74,8 @@
private string? GetSubjectName(EventLogSubjectType type, string id) {
return type switch {
EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
_ => null
EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
_ => null
};
}

View File

@ -14,14 +14,19 @@
<p>Return to <a href="instances">all instances</a>.</p>
}
else {
<h1>Instance: @Instance.Configuration.InstanceName</h1>
<div class="d-flex flex-row align-items-center gap-3 mb-3">
<h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1>
<span class="fs-4 text-muted">//</span>
<div class="mt-2">
<InstanceStatusText Status="Instance.Status" />
</div>
</div>
<div class="d-flex flex-row align-items-center gap-2">
<PermissionView Permission="Permission.ControlInstances">
<button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button>
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button>
<span><!-- extra spacing --></span>
</PermissionView>
<InstanceStatusText Status="Instance.Status" />
<PermissionView Permission="Permission.CreateInstances">
<a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a>
</PermissionView>

View File

@ -6,7 +6,7 @@
@using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Authorization
@using Phantom.Web.Services.Instances
@inherits Phantom.Web.Components.PhantomComponent
@inherits PhantomComponent
@inject AgentManager AgentManager
@inject InstanceManager InstanceManager
@ -16,66 +16,56 @@
<a href="instances/create" class="btn btn-primary" role="button">New Instance</a>
</PermissionView>
<table class="table align-middle">
<thead>
<tr>
<Column Width="200px; 28%">Agent</Column>
<Column Width="200px; 28%">Name</Column>
<Column Width="130px; 11%">Version</Column>
<Column Width="110px; 8%" Class="text-center">Server Port</Column>
<Column Width="110px; 8%" Class="text-center">Rcon Port</Column>
<Column Width=" 90px; 8%" Class="text-end">Memory</Column>
<Column Width="320px">Identifier</Column>
<Column Width="200px; 9%">Status</Column>
<Column Width=" 75px">Actions</Column>
</tr>
</thead>
@if (!instances.IsEmpty) {
<tbody>
@foreach (var (configuration, status, _) in instances) {
var agentName = agentNamesByGuid.TryGetValue(configuration.AgentGuid, out var name) ? name : string.Empty;
var instanceGuid = configuration.InstanceGuid.ToString();
<tr>
<td>@agentName</td>
<td>@configuration.InstanceName</td>
<td>@configuration.MinecraftServerKind @configuration.MinecraftVersion</td>
<td class="text-center">
<code>@configuration.ServerPort</code>
</td>
<td class="text-center">
<code>@configuration.RconPort</code>
</td>
<td class="text-end">
<code>@configuration.MemoryAllocation.InMegabytes MB</code>
</td>
<td>
<code class="text-uppercase">@instanceGuid</code>
</td>
<td>
<InstanceStatusText Status="status" />
</td>
<td>
<a href="instances/@instanceGuid" class="btn btn-info btn-sm">Detail</a>
</td>
</tr>
}
</tbody>
}
@if (instances.IsEmpty) {
<tfoot>
<tr>
<td colspan="9">
No instances.
</td>
</tr>
</tfoot>
}
</table>
<Table TItem="Instance" Items="instances">
<HeaderRow>
<Column Width="40%">Agent</Column>
<Column Width="40%">Name</Column>
<Column MinWidth="215px">Status</Column>
<Column Width="20%">Version</Column>
<Column Class="text-center" MinWidth="110px">Server Port</Column>
<Column Class="text-center" MinWidth="110px">Rcon Port</Column>
<Column Class="text-end" MinWidth="90px">Memory</Column>
<Column MinWidth="75px">Actions</Column>
</HeaderRow>
<ItemRow Context="instance">
@{
var configuration = instance.Configuration;
var agentName = agentNamesByGuid.TryGetValue(configuration.AgentGuid, out var name) ? name : string.Empty;
}
<td>
<p class="fw-semibold">@agentName</p>
<small class="font-monospace text-uppercase">@configuration.AgentGuid.ToString()</small>
</td>
<td>
<p class="fw-semibold">@configuration.InstanceName</p>
<small class="font-monospace text-uppercase">@configuration.InstanceGuid.ToString()</small>
</td>
<td>
<InstanceStatusText Status="instance.Status" />
</td>
<td>@configuration.MinecraftServerKind @configuration.MinecraftVersion</td>
<td class="text-center">
<p class="font-monospace">@configuration.ServerPort.ToString()</p>
</td>
<td class="text-center">
<p class="font-monospace">@configuration.RconPort.ToString()</p>
</td>
<td class="text-end">
<p class="font-monospace">@configuration.MemoryAllocation.InMegabytes.ToString() MB</p>
</td>
<td>
<a href="instances/@configuration.InstanceGuid.ToString()" class="btn btn-info btn-sm">Detail</a>
</td>
</ItemRow>
<NoItemsRow>
No instances found.
</NoItemsRow>
</Table>
@code {
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
private ImmutableArray<Instance> instances = ImmutableArray<Instance>.Empty;
private ImmutableArray<Instance>? instances;
protected override void OnInitialized() {
AgentManager.AgentsChanged.Subscribe(this, agents => {

View File

@ -18,43 +18,36 @@
<AuthorizeView>
<Authorized>
@{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); }
<table class="table align-middle">
<thead>
<tr>
<Column Width="320px">Identifier</Column>
<Column Width="125px; 40%">Username</Column>
<Column Width="125px; 60%">Roles</Column>
@if (canEdit) {
<Column Width="175px">Actions</Column>
}
</tr>
</thead>
<tbody>
@foreach (var user in allUsers) {
var isMe = me == user.Guid;
<tr>
<td>
<code class="text-uppercase">@user.Guid</code>
</td>
@if (isMe) {
<td class="fw-semibold">@user.Name</td>
}
else {
<td>@user.Name</td>
}
<td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td>
@if (canEdit) {
<td>
@if (!isMe) {
<button class="btn btn-primary btn-sm" @onclick="() => userRolesDialog.Show(user)">Edit Roles</button>
<button class="btn btn-danger btn-sm" @onclick="() => userDeleteDialog.Show(user)">Delete...</button>
}
</td>
}
</tr>
<Table TItem="UserInfo" Items="allUsers">
<HeaderRow>
<Column>Username</Column>
<Column Width="100%">Roles</Column>
@if (canEdit) {
<Column MinWidth="175px">Actions</Column>
}
</tbody>
</table>
</HeaderRow>
<ItemRow Context="user">
@{ var isMe = me == user.Guid; }
<td>
<p class="fw-semibold">@user.Name</p>
<small class="font-monospace text-uppercase">@user.Guid.ToString()</small>
</td>
<td>
@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")
</td>
@if (canEdit) {
<td>
@if (!isMe) {
<button class="btn btn-primary btn-sm" @onclick="() => userRolesDialog.Show(user)">Edit Roles</button>
<button class="btn btn-danger btn-sm" @onclick="() => userDeleteDialog.Show(user)">Delete...</button>
}
</td>
}
</ItemRow>
<NoItemsRow>
No users found.
</NoItemsRow>
</Table>
</Authorized>
</AuthorizeView>
@ -67,10 +60,12 @@
@code {
private Guid? me = Guid.Empty;
private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty;
private ImmutableArray<UserInfo>? allUsers;
private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty;
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new();
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new ();
private ImmutableArray<UserInfo> AllUsers => allUsers.GetValueOrDefault(ImmutableArray<UserInfo>.Empty);
private UserRolesDialog userRolesDialog = null!;
private UserDeleteDialog userDeleteDialog = null!;
@ -81,9 +76,10 @@
allRolesByGuid = (await RoleManager.GetAll(CancellationToken)).ToImmutableDictionary(static role => role.Guid, static role => role);
var allUserGuids = allUsers
.Select(static user => user.Guid)
.ToImmutableHashSet();
.Value
.Select(static user => user.Guid)
.ToImmutableHashSet();
foreach (var (userGuid, roleGuids) in await UserRoleManager.GetUserRoles(allUserGuids, CancellationToken)) {
userGuidToRoleDescription[userGuid] = StringifyRoles(roleGuids);
}
@ -102,7 +98,7 @@
}
private Task OnUserAdded(UserInfo user) {
allUsers = allUsers.Add(user);
allUsers = AllUsers.Add(user);
return RefreshUserRoles(user);
}
@ -111,7 +107,7 @@
}
private void OnUserDeleted(UserInfo user) {
allUsers = allUsers.Remove(user);
allUsers = AllUsers.Remove(user);
userGuidToRoleDescription.Remove(user.Guid);
}

View File

@ -1,46 +1,48 @@
@using Phantom.Common.Data.Instance
<nobr>
@switch (Status) {
case InstanceIsOffline:
<text>Offline</text>
<span class="fw-semibold">Offline</span>
break;
case InstanceIsInvalid invalid:
<text>Invalid <sup title="@invalid.Reason">[?]</sup></text>
<span class="fw-semibold text-danger">Invalid <sup title="@invalid.Reason">[?]</sup></span>
break;
case InstanceIsNotRunning:
<text>Not Running</text>
<span class="fw-semibold">Not Running</span>
break;
case InstanceIsDownloading downloading:
<ProgressBar Value="@downloading.Progress" Maximum="100">
Downloading Server (@downloading.Progress%)
<span class="fw-semibold">Downloading Server</span> (@downloading.Progress%)
</ProgressBar>
break;
case InstanceIsLaunching:
<div class="spinner-border spinner-border-sm" role="status"></div>
<text>&nbsp;Launching</text>
<div class="spinner-border" role="status"></div>
<span class="fw-semibold">&nbsp;Launching</span>
break;
case InstanceIsRunning:
<text>Running</text>
<span class="fw-semibold text-success">Running</span>
break;
case InstanceIsRestarting:
<div class="spinner-border spinner-border-sm" role="status"></div>
<text>&nbsp;Restarting</text>
<div class="spinner-border" role="status"></div>
<span class="fw-semibold">&nbsp;Restarting</span>
break;
case InstanceIsStopping:
<div class="spinner-border spinner-border-sm" role="status"></div>
<text>&nbsp;Stopping</text>
<div class="spinner-border" role="status"></div>
<span class="fw-semibold">&nbsp;Stopping</span>
break;
case InstanceIsFailed failed:
<text>Failed <sup title="@failed.Reason.ToSentence()">[?]</sup></text>
<span class="fw-semibold text-danger">Failed <sup title="@failed.Reason.ToSentence()">[?]</sup></span>
break;
}
</nobr>
@code {

View File

@ -26,16 +26,21 @@
flex-direction: column;
position: sticky;
top: 0;
width: 250px;
width: 230px;
min-height: 100vh;
}
}
main {
flex: 1;
min-width: 0;
padding: 1.1rem 1.5rem 0;
}
h1 {
font-weight: 600;
}
h1:focus {
outline: none;
}
@ -51,14 +56,30 @@ code {
.table {
margin-top: 0.5rem;
white-space: nowrap;
}
.table > :not(:first-child) {
border-top: 2px solid #a6a6a6;
}
.table > :not(caption) > * > * {
padding: 0.5rem 0.75rem;
.table th, .table td {
padding: 0.5rem 1.25rem;
}
.table p {
margin: 0;
}
.table small {
display: block;
font-weight: normal;
font-size: 0.825rem;
color: #666;
}
.table small.font-monospace {
font-size: 0.875rem;
}
.form-range {
@ -91,9 +112,19 @@ code {
.text-condensed {
font-size: 0.9rem;
line-height: 1.05rem;
line-height: 1.15rem;
}
.text-condensed code {
font-size: 0.9rem;
}
.horizontal-scroll {
overflow-x: auto;
scrollbar-width: thin;
margin-bottom: 1rem;
}
.horizontal-scroll > .table {
margin-bottom: 0;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long