Merged logs view into edit page. WIP text editor for server files.

dhcp-server
Pat Hartl 2023-04-16 21:13:37 -05:00
parent 1a84c4c51a
commit c60cf157cd
5 changed files with 330 additions and 26 deletions

View File

@ -0,0 +1,31 @@
namespace LANCommander.Extensions
{
public static class FileExtensions
{
public static bool IsBinaryFile(this FileInfo fileInfo, int checkLength = 8000, int requiredConsecutiveNul = 1)
{
const char nulChar = '\0';
int count = 0;
using (var reader = new StreamReader(fileInfo.FullName))
{
for (int i = 0; i < checkLength && !reader.EndOfStream; i++) {
if ((char)reader.Read() == nulChar)
{
count++;
if (count >= requiredConsecutiveNul)
return true;
}
else
{
count = 0;
}
}
}
return false;
}
}
}

View File

@ -1,8 +1,6 @@
@page "/Servers/{id:guid}/Logs"
@using Microsoft.AspNetCore.SignalR.Client
@using Microsoft.AspNetCore.SignalR.Client
@using NLog;
@using XtermBlazor
@attribute [Authorize]
@inject ServerService ServerService
@inject ServerProcessService ServerProcessService
@inject NavigationManager NavigationManager

View File

@ -0,0 +1,152 @@
@using LANCommander.Extensions;
<GridRow>
<GridCol Span="6">
<Tree TItem="FileTreeNode"
DataSource="FileTree"
TitleExpression="x => x.DataItem.Name"
ChildrenExpression="x => x.DataItem.Children"
IsLeafExpression="x => !x.DataItem.HasChildren"
OnDblClick="(args) => OpenFile(args.Node.DataItem)">
</Tree>
</GridCol>
<GridCol Span="18">
<Breadcrumb>
@foreach (var part in CurrentFile.Split(Path.DirectorySeparatorChar))
{
<BreadcrumbItem>@part</BreadcrumbItem>
}
</Breadcrumb>
<StandaloneCodeEditor @ref="Editor" Id="editor" ConstructionOptions="EditorConstructionOptions" />
</GridCol>
</GridRow>
<style>
.monaco-editor-container {
min-height: 600px;
}
</style>
@code {
[Parameter] public string WorkingDirectory { get; set; }
StandaloneCodeEditor? Editor;
IEnumerable<string> Files;
HashSet<FileTreeNode> FileTree { get; set; } = new HashSet<FileTreeNode>();
string CurrentFile = "";
private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
{
return new StandaloneEditorConstructionOptions
{
AutomaticLayout = true,
Theme = "vs-dark"
};
}
protected override async Task OnParametersSetAsync()
{
var root = new FileTreeNode
{
Name = "/",
FullName = WorkingDirectory,
IsExpanded = true
};
root.PopulateChildren(WorkingDirectory);
FileTree.Add(root);
}
private void OpenFile(FileTreeNode node)
{
if (File.Exists(node.FullName))
{
var file = new FileInfo(node.FullName);
CurrentFile = node.FullName;
if (!file.IsBinaryFile())
{
Editor.SetValue(File.ReadAllText(file.FullName));
}
else
{
Editor.SetValue("This file cannot be edited because it is a binary file.");
}
}
}
public class FileTreeNode
{
public string Name { get; set; }
public string FullName { get; set; }
public bool IsExpanded { get; set; } = false;
public bool IsDirectory { get; set; } = false;
public bool HasChildren => Children != null && Children.Count > 0;
public HashSet<FileTreeNode> Children { get; set; } = new HashSet<FileTreeNode>();
public void PopulateChildren(string path)
{
if (Directory.Exists(path))
{
try
{
foreach (var file in Directory.GetFiles(path))
{
var fileInfo = new FileInfo(file);
Children.Add(new FileTreeNode
{
Name = fileInfo.Name,
FullName = fileInfo.FullName,
IsDirectory = false
});
}
}
catch { }
try
{
foreach (var directory in Directory.GetDirectories(path))
{
var directoryInfo = new DirectoryInfo(directory);
var child = new FileTreeNode
{
Name = directoryInfo.Name,
FullName = directoryInfo.FullName,
IsDirectory = true
};
child.PopulateChildren(directoryInfo.FullName);
Children.Add(child);
}
}
catch { }
}
}
public void PopulateChildren(IEnumerable<string> paths)
{
var childPaths = paths.Where(p => p.StartsWith(FullName) && p.EndsWith(Path.PathSeparator));
var directChildren = childPaths.Where(p => p != FullName && p.Substring(FullName.Length + 1).TrimEnd(Path.PathSeparator).Split(Path.PathSeparator).Length == 1);
foreach (var directChild in directChildren)
{
var child = new FileTreeNode()
{
FullName = directChild,
Name = directChild.Substring(FullName.Length).TrimEnd(Path.PathSeparator)
};
child.PopulateChildren(paths);
Children.Add(child);
}
}
}
}

View File

@ -1,11 +1,79 @@
@page "/Servers/{id:guid}/Edit"
@page "/Servers/{id:guid}/Edit/{panel}"
@page "/Servers/Add"
@using LANCommander.Pages.Servers.Components
@inject ServerService ServerService
@inject ServerProcessService ServerProcessService
@inject IMessageService MessageService
@inject NavigationManager NavigationManager
<Card Title="Server Details">
<Body>
<Layout Class="site-layout-background" Style="padding: 24px 0;">
<Sider Class="site-layout-background" Width="200">
<Menu Mode="@MenuMode.Inline" Style="height: 100%;">
<MenuItem RouterLink="@($"/Servers/{Server.Id}/Edit/General")">General</MenuItem>
@if (Server != null && Server.Id != Guid.Empty)
{
<MenuItem RouterLink="@($"/Servers/{Server.Id}/Edit/Logs")">Logs</MenuItem>
<MenuItem RouterLink="@($"/Servers/{Server.Id}/Edit/Files")">Files</MenuItem>
}
</Menu>
</Sider>
<Content>
@if (Panel == "Logs")
{
<PageHeader>
<PageHeaderTitle>Logs</PageHeaderTitle>
<PageHeaderExtra>
<Space Size="@("large")">
<SpaceItem Style="margin-right: 24px;">
@switch (Status)
{
case ServerProcessStatus.Running:
<Badge Status="success" Text="Running" />
break;
case ServerProcessStatus.Starting:
<Badge Status="processing" Text="Starting" />
break;
case ServerProcessStatus.Error:
<Badge Status="error" Text="Error" />
break;
case ServerProcessStatus.Stopped:
default:
<Badge Status="default" Text="Stopped" />
break;
}
</SpaceItem>
@if (Status == ServerProcessStatus.Error || Status == ServerProcessStatus.Stopped)
{
<Button Type="@ButtonType.Primary" OnClick="() => Start()">Start</Button>
}
else
{
<Popconfirm OnConfirm="() => Stop()" Title="Are you sure you want to kill this server process?">
<Button Danger Type="@ButtonType.Primary">Stop</Button>
</Popconfirm>
}
</Space>
</PageHeaderExtra>
</PageHeader>
}
else
{
<PageHeader>
<PageHeaderTitle>@Panel</PageHeaderTitle>
</PageHeader>
}
<div class="site-layout-content">
@if (Panel == "General")
{
<Form Model="@Server" Layout="@FormLayout.Vertical">
<FormItem Label="Name">
<Input @bind-Value="@context.Name" />
@ -23,13 +91,42 @@
<Button Type="@ButtonType.Primary" OnClick="Save" Icon="@IconType.Fill.Save">Save</Button>
</FormItem>
</Form>
</Body>
</Card>
}
@if (Panel == "Logs")
{
<Logs Id="@Server.Id" />
}
@if (Panel == "Files")
{
<TextEditor WorkingDirectory="@Server.WorkingDirectory" />
}
</div>
</Content>
</Layout>
<style>
.site-layout-background {
background: #fff;
}
.site-layout-content {
padding: 0 24px;
}
.ant-layout-content > .ant-page-header-heading {
padding-top: 0;
}
</style>
@code {
[Parameter] public Guid Id { get; set; }
[Parameter] public string Panel { get; set; }
Server Server;
ServerProcessStatus Status;
Timer Timer;
protected override async Task OnInitializedAsync()
{
@ -37,6 +134,32 @@
Server = new Server();
else
Server = await ServerService.Get(Id);
if (String.IsNullOrWhiteSpace(Server.WorkingDirectory))
Server.WorkingDirectory = "C:\\";
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Timer = new Timer(async (object? stateInfo) =>
{
Status = ServerProcessService.GetStatus(Server);
await InvokeAsync(StateHasChanged);
}, new AutoResetEvent(false), 1000, 1000);
}
}
private void Start()
{
ServerProcessService.StartServer(Server);
}
private void Stop()
{
ServerProcessService.StopServer(Server);
}
private async Task Save()

View File

@ -105,12 +105,12 @@
private void Edit(Server server)
{
NavigationManager.NavigateTo($"/Servers/{server.Id}/Edit");
NavigationManager.NavigateTo($"/Servers/{server.Id}/Edit/General");
}
private void Logs(Server server)
{
NavigationManager.NavigateTo($"/Servers/{server.Id}/Logs");
NavigationManager.NavigateTo($"/Servers/{server.Id}/Edit/Logs");
}
private void Start(Server server)