1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2024-11-25 16:42:54 +01:00

Compare commits

...

3 Commits

Author SHA1 Message Date
1ded0e50b2
Redesign Web navigation 2023-12-20 06:11:24 +01:00
e679b17e3b
Redesign Web tables 2023-12-20 06:11:24 +01:00
578ec2d11c
Clean up Web project 2023-12-18 06:04:12 +01:00
42 changed files with 432 additions and 365 deletions

View File

@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis;
namespace Phantom.Utils.Collections; 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 List<TRow> rowList = new();
private readonly Dictionary<TKey, TRow> rowDictionary = new (); private readonly Dictionary<TKey, TRow> rowDictionary = new ();

View File

@ -4,6 +4,6 @@ namespace Phantom.Utils.Runtime;
public static class AssemblyAttributes { public static class AssemblyAttributes {
public static string GetFullVersion(Assembly assembly) { public static string GetFullVersion(Assembly assembly) {
return assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Replace('+', '/') ?? string.Empty; return assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Replace('+', '-') ?? string.Empty;
} }
} }

View File

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

View File

@ -1,9 +1,9 @@
.progress { .progress {
height: 4px; height: 4px;
margin: 0.15rem 0;
} }
.progress-label { .progress-label {
width: 100%; width: 100%;
margin-bottom: 0.15rem;
font-size: 0.9rem; 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

@ -13,4 +13,8 @@
<PackageReference Include="Microsoft.AspNetCore.Components.Web" /> <PackageReference Include="Microsoft.AspNetCore.Components.Web" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Phantom.Web.Services\Phantom.Web.Services.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -6,7 +6,7 @@ using Phantom.Web.Services.Authorization;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
using UserInfo = Phantom.Web.Services.Authentication.UserInfo; using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
namespace Phantom.Web.Base; namespace Phantom.Web.Components;
public abstract class PhantomComponent : ComponentBase, IDisposable { public abstract class PhantomComponent : ComponentBase, IDisposable {
private static readonly ILogger Logger = PhantomLogger.Create<PhantomComponent>(); private static readonly ILogger Logger = PhantomLogger.Create<PhantomComponent>();

View File

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

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

@ -1,5 +1,6 @@
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using Phantom.Web.Components
@using Phantom.Web.Components.Forms @using Phantom.Web.Components.Forms
@using Phantom.Web.Components.Forms.Base @using Phantom.Web.Components.Forms.Base
@using Phantom.Web.Components.Forms.Fields @using Phantom.Web.Components.Forms.Fields

View File

@ -0,0 +1,6 @@
namespace Phantom.Web.Services;
public sealed record ApplicationProperties(
string Version,
byte[] AdministratorToken
);

View File

@ -9,13 +9,13 @@ namespace Phantom.Web.Services.Authentication;
public sealed class UserLoginManager { public sealed class UserLoginManager {
private static readonly ILogger Logger = PhantomLogger.Create<UserLoginManager>(); private static readonly ILogger Logger = PhantomLogger.Create<UserLoginManager>();
private readonly INavigation navigation; private readonly Navigation navigation;
private readonly UserSessionManager sessionManager; private readonly UserSessionManager sessionManager;
private readonly UserSessionBrowserStorage sessionBrowserStorage; private readonly UserSessionBrowserStorage sessionBrowserStorage;
private readonly CustomAuthenticationStateProvider authenticationStateProvider; private readonly CustomAuthenticationStateProvider authenticationStateProvider;
private readonly ControllerConnection controllerConnection; private readonly ControllerConnection controllerConnection;
public UserLoginManager(INavigation navigation, UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage, CustomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) { public UserLoginManager(Navigation navigation, UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage, CustomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) {
this.navigation = navigation; this.navigation = navigation;
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
this.sessionBrowserStorage = sessionBrowserStorage; this.sessionBrowserStorage = sessionBrowserStorage;

View File

@ -1,9 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace Phantom.Web.Services;
public interface INavigation {
string BasePath { get; }
bool GetQueryParameter(string key, [MaybeNullWhen(false)] out string value);
Task NavigateTo(string url, bool forceLoad = false);
}

View File

@ -2,11 +2,10 @@
using System.Web; using System.Web;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Routing;
using Phantom.Web.Services;
namespace Phantom.Web.Base; namespace Phantom.Web.Services;
sealed class Navigation : INavigation { public sealed class Navigation {
public static Func<IServiceProvider, Navigation> Create(string basePath) { public static Func<IServiceProvider, Navigation> Create(string basePath) {
return provider => new Navigation(basePath, provider.GetRequiredService<NavigationManager>()); return provider => new Navigation(basePath, provider.GetRequiredService<NavigationManager>());
} }
@ -28,6 +27,10 @@ sealed class Navigation : INavigation {
return value != null; return value != null;
} }
public string CreateReturnUrl() {
return navigationManager.ToBaseRelativePath(navigationManager.Uri).TrimEnd('/');
}
public async Task NavigateTo(string url, bool forceLoad = false) { public async Task NavigateTo(string url, bool forceLoad = false) {
var newPath = BasePath + url; var newPath = BasePath + url;

View File

@ -1,6 +1,5 @@
@using Phantom.Web.Services @using Phantom.Web.Services
@inject INavigation Nav @inject Navigation Navigation
@inject NavigationManager NavigationManager
<CascadingAuthenticationState> <CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly"> <Router AppAssembly="@typeof(App).Assembly">
@ -12,8 +11,7 @@
<p role="alert">You do not have permission to visit this page.</p> <p role="alert">You do not have permission to visit this page.</p>
} }
else { else {
var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri).TrimEnd('/'); Navigation.NavigateTo("login" + QueryString.Create("return", Navigation.CreateReturnUrl()), forceLoad: true);
Nav.NavigateTo("login" + QueryString.Create("return", returnUrl), forceLoad: true);
} }
</NotAuthorized> </NotAuthorized>
</AuthorizeRouteView> </AuthorizeRouteView>

View File

@ -1,7 +0,0 @@
using ILogger = Serilog.ILogger;
namespace Phantom.Web;
sealed record Configuration(ILogger Logger, string Host, ushort Port, string BasePath, string DataProtectionKeyFolderPath, CancellationToken CancellationToken) {
public string HttpUrl => "http://" + Host + ":" + Port;
}

View File

@ -1,11 +1,15 @@
@using Phantom.Web.Services.Authorization @using Phantom.Web.Services
@using Phantom.Web.Services.Authorization
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@inject ServiceConfiguration Configuration @inject ApplicationProperties ApplicationProperties
@inject PermissionManager PermissionManager @inject PermissionManager PermissionManager
<div class="navbar navbar-dark"> <div class="navbar navbar-dark">
<div class="container-fluid"> <div class="container-fluid">
<div class="pt-1 pb-2">
<a class="navbar-brand" href="">Phantom Panel</a> <a class="navbar-brand" href="">Phantom Panel</a>
<small class="navbar-text">Version&nbsp;@ApplicationProperties.Version</small>
</div>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
@ -44,9 +48,6 @@
</Authorized> </Authorized>
</AuthorizeView> </AuthorizeView>
</nav> </nav>
<footer>
Build @Configuration.Version
</footer>
</div> </div>
@code { @code {

View File

@ -1,7 +1,7 @@
@page @page
@using Phantom.Web.Services @using Phantom.Web.Services
@model Phantom.Web.Layout.ErrorModel @model Phantom.Web.Layout.ErrorModel
@inject INavigation Navigation @inject Navigation Navigation
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">

View File

@ -1,7 +1,7 @@
@using Phantom.Web.Services @using Phantom.Web.Services
@namespace Phantom.Web.Layout @namespace Phantom.Web.Layout
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject INavigation Navigation @inject Navigation Navigation
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">

View File

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

View File

@ -5,55 +5,49 @@
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Users @using Phantom.Web.Services.Users
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject AuditLogManager AuditLogManager @inject AuditLogManager AuditLogManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@inject UserManager UserManager @inject UserManager UserManager
<h1>Audit Log</h1> <h1>Audit Log</h1>
<table class="table"> <Table TItem="AuditLogItem" Items="logItems">
<thead> <HeaderRow>
<tr> <Column Class="text-end" MinWidth="200px">Time</Column>
<Column Width="165px" Class="text-end">Time</Column> <Column>User</Column>
<Column Width="320px; 20%">User</Column> <Column>Event Type</Column>
<Column Width="160px">Event Type</Column> <Column>Subject</Column>
<Column Width="320px; 20%">Subject</Column> <Column Width="100%">Data</Column>
<Column Width="100px; 60%">Data</Column> </HeaderRow>
</tr> <ItemRow Context="logItem">
</thead>
<tbody>
@foreach (var logItem in logItems) {
DateTimeOffset time = logItem.UtcTime.ToLocalTime();
<tr>
<td class="text-end"> <td class="text-end">
<time datetime="@time.ToString("o")">@time.ToString()</time> <TimeWithOffset Time="logItem.UtcTime.ToLocalTime()" />
</td> </td>
<td> <td>
@(logItem.UserName ?? "-") <p class="fw-semibold">@(logItem.UserName ?? "-")</p>
<br> <small class="font-monospace text-uppercase">@logItem.UserGuid.ToString()</small>
<code class="text-uppercase">@logItem.UserGuid</code>
</td> </td>
<td>@logItem.EventType.ToNiceString()</td>
<td> <td>
@if (logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName) { <p>@logItem.EventType.ToNiceString()</p>
@subjectName </td>
<br> <td>
} <p class="fw-semibold">@(logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName ? subjectName : "-")</p>
<code class="text-uppercase">@(logItem.SubjectId ?? "-")</code> <small class="font-monospace text-uppercase">@(logItem.SubjectId ?? "-")</small>
</td> </td>
<td> <td>
<code>@logItem.JsonData</code> <code>@logItem.JsonData</code>
</td> </td>
</tr> </ItemRow>
} <NoItemsRow>
</tbody> No audit log entries found.
</table> </NoItemsRow>
</Table>
@code { @code {
private CancellationTokenSource? initializationCancellationTokenSource; private CancellationTokenSource? initializationCancellationTokenSource;
private ImmutableArray<AuditLogItem> logItems = ImmutableArray<AuditLogItem>.Empty; private ImmutableArray<AuditLogItem>? logItems;
private ImmutableDictionary<Guid, string>? userNamesByGuid; private ImmutableDictionary<Guid, string>? userNamesByGuid;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;

View File

@ -6,35 +6,29 @@
@using Phantom.Web.Services.Agents @using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Events @using Phantom.Web.Services.Events
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject AgentManager AgentManager @inject AgentManager AgentManager
@inject EventLogManager EventLogManager @inject EventLogManager EventLogManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
<h1>Event Log</h1> <h1>Event Log</h1>
<table class="table"> <Table TItem="EventLogItem" Items="logItems">
<thead> <HeaderRow>
<tr> <Column Class="text-end" MinWidth="200px">Time</Column>
<Column Width="165px" Class="text-end">Time</Column> <Column>Agent</Column>
<Column Width="320px; 20%">Agent</Column> <Column>Event Type</Column>
<Column Width="160px">Event Type</Column> <Column>Subject</Column>
<Column Width="320px; 20%">Subject</Column> <Column Width="100%">Data</Column>
<Column Width="100px; 60%">Data</Column> </HeaderRow>
</tr> <ItemRow Context="logItem">
</thead>
<tbody>
@foreach (var logItem in logItems) {
DateTimeOffset time = logItem.UtcTime.ToLocalTime();
<tr>
<td class="text-end"> <td class="text-end">
<time datetime="@time.ToString("o")">@time.ToString()</time> <TimeWithOffset Time="logItem.UtcTime.ToLocalTime()" />
</td> </td>
<td> <td>
@if (logItem.AgentGuid is {} agentGuid) { @if (logItem.AgentGuid is {} agentGuid) {
@(GetAgentName(agentGuid)) <p class="fw-semibold">@(GetAgentName(agentGuid))</p>
<br> <small class="font-monospace text-uppercase">@agentGuid.ToString()</small>
<code class="text-uppercase">@agentGuid</code>
} }
else { else {
<text>-</text> <text>-</text>
@ -42,24 +36,22 @@
</td> </td>
<td>@logItem.EventType.ToNiceString()</td> <td>@logItem.EventType.ToNiceString()</td>
<td> <td>
@if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) { <p class="fw-semibold">@(GetSubjectName(logItem.SubjectType, logItem.SubjectId) ?? "-")</p>
@subjectName <small class="font-monospace text-uppercase">@(logItem.SubjectId)</small>
<br>
}
<code class="text-uppercase">@logItem.SubjectId</code>
</td> </td>
<td> <td>
<code>@logItem.JsonData</code> <code>@logItem.JsonData</code>
</td> </td>
</tr> </ItemRow>
} <NoItemsRow>
</tbody> No event log entries found.
</table> </NoItemsRow>
</Table>
@code { @code {
private CancellationTokenSource? initializationCancellationTokenSource; 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> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;

View File

@ -6,7 +6,7 @@
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Authorization @using Phantom.Web.Services.Authorization
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@if (Instance == null) { @if (Instance == null) {
@ -14,14 +14,19 @@
<p>Return to <a href="instances">all instances</a>.</p> <p>Return to <a href="instances">all instances</a>.</p>
} }
else { 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"> <div class="d-flex flex-row align-items-center gap-2">
<PermissionView Permission="Permission.ControlInstances"> <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-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> <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> <span><!-- extra spacing --></span>
</PermissionView> </PermissionView>
<InstanceStatusText Status="Instance.Status" />
<PermissionView Permission="Permission.CreateInstances"> <PermissionView Permission="Permission.CreateInstances">
<a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a> <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a>
</PermissionView> </PermissionView>

View File

@ -3,7 +3,7 @@
@using Phantom.Common.Data.Instance @using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@if (InstanceConfiguration == null) { @if (InstanceConfiguration == null) {

View File

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

View File

@ -3,7 +3,7 @@
@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Authentication
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject INavigation Navigation @inject Navigation Navigation
@inject UserLoginManager LoginManager @inject UserLoginManager LoginManager
<h1>Login</h1> <h1>Login</h1>

View File

@ -1,5 +1,6 @@
@page "/setup" @page "/setup"
@using Phantom.Utils.Tasks @using Phantom.Utils.Tasks
@using Phantom.Web.Services
@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Authentication
@using Phantom.Web.Services.Rpc @using Phantom.Web.Services.Rpc
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@ -9,7 +10,7 @@
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults @using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject ServiceConfiguration ServiceConfiguration @inject ApplicationProperties ApplicationProperties
@inject UserLoginManager LoginManager @inject UserLoginManager LoginManager
@inject ControllerConnection ControllerConnection @inject ControllerConnection ControllerConnection
@ -83,7 +84,7 @@
return false; return false;
} }
return CryptographicOperations.FixedTimeEquals(formTokenBytes, ServiceConfiguration.AdministratorToken); return CryptographicOperations.FixedTimeEquals(formTokenBytes, ApplicationProperties.AdministratorToken);
} }
private async Task<Result<string>> CreateOrUpdateAdministrator() { private async Task<Result<string>> CreateOrUpdateAdministrator() {

View File

@ -4,7 +4,7 @@
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Users @using Phantom.Web.Services.Users
@using Phantom.Web.Services.Authorization @using Phantom.Web.Services.Authorization
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject UserManager UserManager @inject UserManager UserManager
@inject RoleManager RoleManager @inject RoleManager RoleManager
@inject UserRoleManager UserRoleManager @inject UserRoleManager UserRoleManager
@ -18,31 +18,23 @@
<AuthorizeView> <AuthorizeView>
<Authorized> <Authorized>
@{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); } @{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); }
<table class="table align-middle"> <Table TItem="UserInfo" Items="allUsers">
<thead> <HeaderRow>
<tr> <Column>Username</Column>
<Column Width="320px">Identifier</Column> <Column Width="100%">Roles</Column>
<Column Width="125px; 40%">Username</Column>
<Column Width="125px; 60%">Roles</Column>
@if (canEdit) { @if (canEdit) {
<Column Width="175px">Actions</Column> <Column MinWidth="175px">Actions</Column>
} }
</tr> </HeaderRow>
</thead> <ItemRow Context="user">
<tbody> @{ var isMe = me == user.Guid; }
@foreach (var user in allUsers) {
var isMe = me == user.Guid;
<tr>
<td> <td>
<code class="text-uppercase">@user.Guid</code> <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> </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) { @if (canEdit) {
<td> <td>
@if (!isMe) { @if (!isMe) {
@ -51,10 +43,11 @@
} }
</td> </td>
} }
</tr> </ItemRow>
} <NoItemsRow>
</tbody> No users found.
</table> </NoItemsRow>
</Table>
</Authorized> </Authorized>
</AuthorizeView> </AuthorizeView>
@ -67,10 +60,12 @@
@code { @code {
private Guid? me = Guid.Empty; 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 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 UserRolesDialog userRolesDialog = null!;
private UserDeleteDialog userDeleteDialog = null!; private UserDeleteDialog userDeleteDialog = null!;
@ -81,6 +76,7 @@
allRolesByGuid = (await RoleManager.GetAll(CancellationToken)).ToImmutableDictionary(static role => role.Guid, static role => role); allRolesByGuid = (await RoleManager.GetAll(CancellationToken)).ToImmutableDictionary(static role => role.Guid, static role => role);
var allUserGuids = allUsers var allUserGuids = allUsers
.Value
.Select(static user => user.Guid) .Select(static user => user.Guid)
.ToImmutableHashSet(); .ToImmutableHashSet();
@ -102,7 +98,7 @@
} }
private Task OnUserAdded(UserInfo user) { private Task OnUserAdded(UserInfo user) {
allUsers = allUsers.Add(user); allUsers = AllUsers.Add(user);
return RefreshUserRoles(user); return RefreshUserRoles(user);
} }
@ -111,7 +107,7 @@
} }
private void OnUserDeleted(UserInfo user) { private void OnUserDeleted(UserInfo user) {
allUsers = allUsers.Remove(user); allUsers = AllUsers.Remove(user);
userGuidToRoleDescription.Remove(user.Guid); userGuidToRoleDescription.Remove(user.Guid);
} }

View File

@ -10,6 +10,7 @@ using Phantom.Utils.Rpc.Sockets;
using Phantom.Utils.Runtime; using Phantom.Utils.Runtime;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
using Phantom.Web; using Phantom.Web;
using Phantom.Web.Services;
using Phantom.Web.Services.Rpc; using Phantom.Web.Services.Rpc;
var shutdownCancellationTokenSource = new CancellationTokenSource(); var shutdownCancellationTokenSource = new CancellationTokenSource();
@ -48,15 +49,15 @@ try {
var (controllerCertificate, webToken) = webKey.Value; var (controllerCertificate, webToken) = webKey.Value;
var administratorToken = TokenGenerator.Create(60);
var applicationProperties = new ApplicationProperties(fullVersion, TokenGenerator.GetBytesOrThrow(administratorToken));
var rpcConfiguration = new RpcConfiguration("Rpc", controllerHost, controllerPort, controllerCertificate); var rpcConfiguration = new RpcConfiguration("Rpc", controllerHost, controllerPort, controllerCertificate);
var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken)); var rpcSocket = RpcClientSocket.Connect(rpcConfiguration, WebMessageRegistries.Definitions, new RegisterWebMessage(webToken));
var configuration = new Configuration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, dataProtectionKeysPath, shutdownCancellationToken); var webConfiguration = new WebLauncher.Configuration(PhantomLogger.Create("Web"), webServerHost, webServerPort, webBasePath, dataProtectionKeysPath, shutdownCancellationToken);
var administratorToken = TokenGenerator.Create(60);
var taskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Web")); var taskManager = new TaskManager(PhantomLogger.Create<TaskManager>("Web"));
var serviceConfiguration = new ServiceConfiguration(fullVersion, TokenGenerator.GetBytesOrThrow(administratorToken), shutdownCancellationToken); var webApplication = WebLauncher.CreateApplication(webConfiguration, taskManager, applicationProperties, rpcSocket.Connection);
var webApplication = WebLauncher.CreateApplication(configuration, taskManager, serviceConfiguration, rpcSocket.Connection);
MessageListener messageListener; MessageListener messageListener;
await using (var scope = webApplication.Services.CreateAsyncScope()) { await using (var scope = webApplication.Services.CreateAsyncScope()) {
@ -77,9 +78,9 @@ try {
PhantomLogger.Root.InformationHeading("Launching Phantom Panel web..."); PhantomLogger.Root.InformationHeading("Launching Phantom Panel web...");
PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken); PhantomLogger.Root.Information("Your administrator token is: {AdministratorToken}", administratorToken);
PhantomLogger.Root.Information("For administrator setup, visit: {HttpUrl}{SetupPath}", configuration.HttpUrl, configuration.BasePath + "setup"); PhantomLogger.Root.Information("For administrator setup, visit: {HttpUrl}{SetupPath}", webConfiguration.HttpUrl, webConfiguration.BasePath + "setup");
await WebLauncher.Launch(configuration, webApplication); await WebLauncher.Launch(webConfiguration, webApplication);
} finally { } finally {
shutdownCancellationTokenSource.Cancel(); shutdownCancellationTokenSource.Cancel();
await taskManager.Stop(); await taskManager.Stop();

View File

@ -1,7 +0,0 @@
namespace Phantom.Web;
public sealed record ServiceConfiguration(
string Version,
byte[] AdministratorToken,
CancellationToken CancellationToken
);

View File

@ -15,8 +15,8 @@
@using Phantom.Web.Services.Agents @using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Rpc @using Phantom.Web.Services.Rpc
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject INavigation Nav @inject Navigation Navigation
@inject ControllerConnection ControllerConnection @inject ControllerConnection ControllerConnection
@inject AgentManager AgentManager @inject AgentManager AgentManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@ -342,7 +342,7 @@
var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instance, CancellationToken); var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instance, CancellationToken);
if (result.Is(CreateOrUpdateInstanceResult.Success)) { if (result.Is(CreateOrUpdateInstanceResult.Success)) {
await Nav.NavigateTo("instances/" + instance.InstanceGuid); await Navigation.NavigateTo("instances/" + instance.InstanceGuid);
} }
else { else {
form.SubmitModel.StopSubmitting(result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence)); form.SubmitModel.StopSubmitting(result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));

View File

@ -1,7 +1,7 @@
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Common.Data.Replies @using Phantom.Common.Data.Replies
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
<Form Model="form" OnSubmit="ExecuteCommand"> <Form Model="form" OnSubmit="ExecuteCommand">

View File

@ -3,7 +3,7 @@
@using System.Diagnostics @using System.Diagnostics
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject IJSRuntime Js; @inject IJSRuntime Js;
@inject InstanceLogManager InstanceLogManager; @inject InstanceLogManager InstanceLogManager;

View File

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

View File

@ -3,7 +3,7 @@
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@using Phantom.Common.Data.Minecraft @using Phantom.Common.Data.Minecraft
@using Phantom.Common.Data.Replies @using Phantom.Common.Data.Replies
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject IJSRuntime Js; @inject IJSRuntime Js;
@inject InstanceManager InstanceManager; @inject InstanceManager InstanceManager;

View File

@ -2,7 +2,7 @@
@using Phantom.Common.Data.Web.Users.CreateUserResults @using Phantom.Common.Data.Web.Users.CreateUserResults
@using Phantom.Web.Services.Users @using Phantom.Web.Services.Users
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@inherits PhantomComponent @inherits Phantom.Web.Components.PhantomComponent
@inject IJSRuntime Js; @inject IJSRuntime Js;
@inject UserManager UserManager; @inject UserManager UserManager;

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Web.Base; using Phantom.Web.Components;
using Phantom.Web.Components.Forms; using Phantom.Web.Components.Forms;
namespace Phantom.Web.Shared; namespace Phantom.Web.Shared;

View File

@ -2,14 +2,18 @@
using Phantom.Common.Messages.Web; using Phantom.Common.Messages.Web;
using Phantom.Utils.Rpc.Runtime; using Phantom.Utils.Rpc.Runtime;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
using Phantom.Web.Base;
using Phantom.Web.Services; using Phantom.Web.Services;
using Serilog; using Serilog;
using ILogger = Serilog.ILogger;
namespace Phantom.Web; namespace Phantom.Web;
static class WebLauncher { static class WebLauncher {
public static WebApplication CreateApplication(Configuration config, TaskManager taskManager, ServiceConfiguration serviceConfiguration, RpcConnectionToServer<IMessageToControllerListener> controllerConnection) { internal sealed record Configuration(ILogger Logger, string Host, ushort Port, string BasePath, string DataProtectionKeyFolderPath, CancellationToken CancellationToken) {
public string HttpUrl => "http://" + Host + ":" + Port;
}
internal static WebApplication CreateApplication(Configuration config, TaskManager taskManager, ApplicationProperties applicationProperties, RpcConnectionToServer<IMessageToControllerListener> controllerConnection) {
var assembly = typeof(WebLauncher).Assembly; var assembly = typeof(WebLauncher).Assembly;
var builder = WebApplication.CreateBuilder(new WebApplicationOptions { var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
ApplicationName = assembly.GetName().Name, ApplicationName = assembly.GetName().Name,
@ -26,12 +30,12 @@ static class WebLauncher {
} }
builder.Services.AddSingleton(taskManager); builder.Services.AddSingleton(taskManager);
builder.Services.AddSingleton(serviceConfiguration); builder.Services.AddSingleton(applicationProperties);
builder.Services.AddSingleton(controllerConnection); builder.Services.AddSingleton(controllerConnection);
builder.Services.AddPhantomServices(); builder.Services.AddPhantomServices();
builder.Services.AddSingleton<IHostLifetime>(new NullLifetime()); builder.Services.AddSingleton<IHostLifetime>(new NullLifetime());
builder.Services.AddScoped<INavigation>(Navigation.Create(config.BasePath)); builder.Services.AddScoped(Navigation.Create(config.BasePath));
builder.Services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(config.DataProtectionKeyFolderPath)); builder.Services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(config.DataProtectionKeyFolderPath));
@ -41,7 +45,7 @@ static class WebLauncher {
return builder.Build(); return builder.Build();
} }
public static Task Launch(Configuration config, WebApplication application) { internal static Task Launch(Configuration config, WebApplication application) {
var logger = config.Logger; var logger = config.Logger;
application.UseSerilogRequestLogging(); application.UseSerilogRequestLogging();

View File

@ -6,8 +6,7 @@
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Phantom.Web @using Phantom.Web.Components
@using Phantom.Web.Base
@using Phantom.Web.Components.Dialogs @using Phantom.Web.Components.Dialogs
@using Phantom.Web.Components.Forms @using Phantom.Web.Components.Forms
@using Phantom.Web.Components.Graphics @using Phantom.Web.Components.Graphics

View File

@ -14,6 +14,8 @@
.sidebar { .sidebar {
background-image: linear-gradient(180deg, #0f6477 200px, #1b3d59 1000px); background-image: linear-gradient(180deg, #0f6477 200px, #1b3d59 1000px);
border-right: 1px solid rgba(2, 39, 47, 0.5);
box-shadow: 0 0 2px #093c47;
} }
@media (min-width: 960px) { @media (min-width: 960px) {
@ -26,16 +28,22 @@
flex-direction: column; flex-direction: column;
position: sticky; position: sticky;
top: 0; top: 0;
width: 250px; width: 230px;
min-height: 100vh; height: 100vh;
} }
} }
main { main {
flex: 1; flex: 1;
min-width: 0;
padding: 1.1rem 1.5rem 0; padding: 1.1rem 1.5rem 0;
} }
h1 {
font-size: 2rem;
font-weight: 600;
}
h1:focus { h1:focus {
outline: none; outline: none;
} }
@ -51,14 +59,30 @@ code {
.table { .table {
margin-top: 0.5rem; margin-top: 0.5rem;
white-space: nowrap;
} }
.table > :not(:first-child) { .table > :not(:first-child) {
border-top: 2px solid #a6a6a6; border-top: 2px solid #a6a6a6;
} }
.table > :not(caption) > * > * { .table th, .table td {
padding: 0.5rem 0.75rem; 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 { .form-range {
@ -91,9 +115,19 @@ code {
.text-condensed { .text-condensed {
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1.05rem; line-height: 1.15rem;
} }
.text-condensed code { .text-condensed code {
font-size: 0.9rem; 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