mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 16:42:54 +01:00
Compare commits
3 Commits
2471dc04f1
...
1ded0e50b2
Author | SHA1 | Date | |
---|---|---|---|
1ded0e50b2 | |||
e679b17e3b | |||
578ec2d11c |
@ -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 ();
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
Web/Phantom.Web.Bootstrap/src/bootstrap.scss
vendored
6
Web/Phantom.Web.Bootstrap/src/bootstrap.scss
vendored
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
14
Web/Phantom.Web.Components/Graphics/TimeWithOffset.razor
Normal file
14
Web/Phantom.Web.Components/Graphics/TimeWithOffset.razor
Normal 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; }
|
||||||
|
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>();
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
58
Web/Phantom.Web.Components/Tables/Table.razor
Normal file
58
Web/Phantom.Web.Components/Tables/Table.razor
Normal 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; }
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
6
Web/Phantom.Web.Services/ApplicationProperties.cs
Normal file
6
Web/Phantom.Web.Services/ApplicationProperties.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Phantom.Web.Services;
|
||||||
|
|
||||||
|
public sealed record ApplicationProperties(
|
||||||
|
string Version,
|
||||||
|
byte[] AdministratorToken
|
||||||
|
);
|
@ -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;
|
||||||
|
@ -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);
|
|
||||||
}
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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 @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 {
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
@ -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 => {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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) {
|
||||||
|
@ -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 => {
|
||||||
|
@ -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>
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
namespace Phantom.Web;
|
|
||||||
|
|
||||||
public sealed record ServiceConfiguration(
|
|
||||||
string Version,
|
|
||||||
byte[] AdministratorToken,
|
|
||||||
CancellationToken CancellationToken
|
|
||||||
);
|
|
@ -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));
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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> Launching</text>
|
<span class="fw-semibold"> 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> Restarting</text>
|
<span class="fw-semibold"> 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> Stopping</text>
|
<span class="fw-semibold"> 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 {
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user