Преглед изворни кода

Merge pull request #87 from Timmoth/webui-embedded-cli-tool

Added YAML edit page
Tim Jones пре 1 месец
родитељ
комит
4d2a87f91f

+ 5 - 4
RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs

@@ -170,8 +170,9 @@ public sealed class YamlResourceCollection(
         return map;
     }
 
-    private class YamlRoot
-    {
-        public List<Resource>? Resources { get; set; }
-    }
+
 }
+public class YamlRoot
+{
+    public List<Resource>? Resources { get; set; }
+}

+ 1 - 1
RackPeek.Web.Viewer/Pages/Home.razor

@@ -3,6 +3,7 @@
 @using RackPeek.Domain.Resources.Hardware
 @using RackPeek.Domain.Resources.Services.UseCases
 @using RackPeek.Domain.Resources.SystemResources.UseCases
+@using Shared.Rcl
 @inject GetSystemSummaryUseCase SystemSummaryUseCase
 @inject GetServiceSummaryUseCase ServiceSummaryUseCase
 @inject GetHardwareUseCaseSummary HardwareSummaryUseCase
@@ -10,7 +11,6 @@
 <PageTitle>Home</PageTitle>
 
 <div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6">
-
     @if (_loading)
     {
         <div class="text-zinc-500">loading summary…</div>

+ 3 - 0
Shared.Rcl/Layout/MainLayout.razor

@@ -22,6 +22,9 @@
             <NavLink href="cli" class="hover:text-emerald-400" activeClass="text-emerald-400 font-semibold">
                 CLI
             </NavLink>
+            <NavLink href="yaml" class="hover:text-emerald-400" activeClass="text-emerald-400 font-semibold">
+                Yaml
+            </NavLink>
             <NavLink href="hardware/tree" class="hover:text-emerald-400" activeClass="text-emerald-400 font-semibold">
                 Hardware
             </NavLink>

+ 244 - 0
Shared.Rcl/YamlFileComponent.razor

@@ -0,0 +1,244 @@
+@using System.Collections.Specialized
+@using RackPeek.Domain.Persistence
+@using RackPeek.Domain.Persistence.Yaml
+@using RackPeek.Domain.Resources
+@using RackPeek.Domain.Resources.Models
+@using RackPeek.Domain.Resources.Services
+@using RackPeek.Domain.Resources.SystemResources
+@using RackPeek.Yaml
+@using YamlDotNet.RepresentationModel
+@using YamlDotNet.Serialization
+@using YamlDotNet.Serialization.NamingConventions
+@using Router = RackPeek.Domain.Resources.Models.Router
+@inject ITextFileStore FileStore
+@inject IResourceCollection Resources
+
+<div class="border border-zinc-800 rounded p-4 bg-zinc-900">
+    <div class="flex justify-between items-center mb-3">
+
+        <div class="text-zinc-100">
+            @Title
+        </div>
+
+        <div class="flex gap-3 text-xs">
+            @if (!_isEditing)
+            {
+                <button class="text-zinc-400 hover:text-zinc-200"
+                        @onclick="BeginEdit">
+                    Edit
+                </button>
+            }
+            else
+            {
+                <button class="text-emerald-400 hover:text-emerald-300"
+                        @onclick="Save">
+                    Save
+                </button>
+
+                <button class="text-zinc-500 hover:text-zinc-300"
+                        @onclick="Cancel">
+                    Cancel
+                </button>
+            }
+        </div>
+    </div>
+
+    @if (!_exists)
+    {
+        <div class="text-red-400 text-sm">
+            File does not exist.
+        </div>
+    }
+    else if (_isEditing)
+    {
+        <textarea class="w-full input font-mono text-xs"
+          style="min-height: 40rem"
+          @bind="_editText">
+        </textarea>
+
+
+        @if (!string.IsNullOrEmpty(_validationError))
+        {
+            <div class="mt-2 text-red-400 text-xs">
+                @_validationError
+            </div>
+        }
+    }
+    else
+    {
+        <pre class="text-zinc-300 text-xs whitespace-pre-wrap">@_currentText</pre>
+    }
+</div>
+
+<ConfirmModal
+    IsOpen="_confirmDeleteOpen"
+    IsOpenChanged="v => _confirmDeleteOpen = v"
+    Title="Delete File"
+    ConfirmText="Delete"
+    ConfirmClass="bg-red-600 hover:bg-red-500"
+    OnConfirm="DeleteFile">
+    Are you sure you want to delete <strong>@Path</strong>?
+</ConfirmModal>
+
+@code {
+
+    [Parameter, EditorRequired]
+    public string Path { get; set; } = default!;
+
+    [Parameter]
+    public string Title { get; set; } = "Edit YAML";
+
+    [Parameter]
+    public EventCallback<string> OnDeleted { get; set; }
+
+    bool _isEditing;
+    bool _exists;
+    bool _confirmDeleteOpen;
+
+    string _currentText = "";
+    string _editText = "";
+    string? _validationError;
+
+    protected override async Task OnParametersSetAsync()
+    {
+        await Load();
+    }
+
+    async Task Load()
+    {
+        _exists = await FileStore.ExistsAsync(Path);
+
+        if (!_exists)
+            return;
+
+        _currentText = await FileStore.ReadAllTextAsync(Path);
+    }
+
+    void BeginEdit()
+    {
+        _editText = _currentText;
+        _validationError = null;
+        _isEditing = true;
+    }
+
+    void Cancel()
+    {
+        _isEditing = false;
+        _validationError = null;
+    }
+    async Task Save()
+    {
+        if (!ValidateYamlRoundTrip(_editText, out var error))
+        {
+            _validationError = error;
+            return;
+        }
+
+        await FileStore.WriteAllTextAsync(Path, _editText);
+
+        await Resources.LoadAsync();
+
+        _currentText = _editText;
+        _isEditing = false;
+    }
+
+
+    void ConfirmDelete()
+    {
+        _confirmDeleteOpen = true;
+    }
+
+    async Task DeleteFile()
+    {
+        _confirmDeleteOpen = false;
+
+        // if your store supports delete, call it here
+        await FileStore.WriteAllTextAsync(Path, "");
+
+        if (OnDeleted.HasDelegate)
+            await OnDeleted.InvokeAsync(Path);
+    }
+private bool ValidateYamlRoundTrip(string yaml, out string? error)
+{
+    try
+    {
+        if (string.IsNullOrWhiteSpace(yaml))
+        {
+            error = "YAML is empty.";
+            return false;
+        }
+
+        // ---------- DESERIALIZER (same as resource loader) ----------
+        var deserializer = new DeserializerBuilder()
+            .WithNamingConvention(CamelCaseNamingConvention.Instance)
+            .WithCaseInsensitivePropertyMatching()
+            .WithTypeConverter(new StorageSizeYamlConverter())
+            .WithTypeDiscriminatingNodeDeserializer(options =>
+            {
+                options.AddKeyValueTypeDiscriminator<Resource>("kind", new Dictionary<string, Type>
+                {
+                    { Server.KindLabel, typeof(Server) },
+                    { Switch.KindLabel, typeof(Switch) },
+                    { Firewall.KindLabel, typeof(Firewall) },
+                    { Router.KindLabel, typeof(Router) },
+                    { Desktop.KindLabel, typeof(Desktop) },
+                    { Laptop.KindLabel, typeof(Laptop) },
+                    { AccessPoint.KindLabel, typeof(AccessPoint) },
+                    { RackPeek.Domain.Resources.Models.Ups.KindLabel, typeof(RackPeek.Domain.Resources.Models.Ups) },
+                    { SystemResource.KindLabel, typeof(SystemResource) },
+                    { Service.KindLabel, typeof(Service) }
+                });
+            })
+            .Build();
+
+        var root = deserializer.Deserialize<YamlRoot>(yaml);
+
+        if (root?.Resources == null)
+        {
+            error = "No resources section found.";
+            return false;
+        }
+
+        // ---------- SERIALIZE AGAIN ----------
+        var serializer = new SerializerBuilder()
+            .WithNamingConvention(CamelCaseNamingConvention.Instance)
+            .Build();
+
+        var payload = new OrderedDictionary
+        {
+            ["resources"] = root.Resources
+        };
+
+        var roundTripYaml = serializer.Serialize(payload);
+
+        // ---------- DESERIALIZE AGAIN ----------
+        var root2 = deserializer.Deserialize<YamlRoot>(roundTripYaml);
+
+        if (root2?.Resources == null)
+        {
+            error = "Round-trip serialization failed.";
+            return false;
+        }
+
+        // ---------- DUPLICATE NAME CHECK ----------
+        var dup = root2.Resources
+            .GroupBy(r => r.Name, StringComparer.OrdinalIgnoreCase)
+            .FirstOrDefault(g => g.Count() > 1);
+
+        if (dup != null)
+        {
+            error = $"Duplicate resource name: '{dup.Key}'";
+            return false;
+        }
+
+        error = null;
+        return true;
+    }
+    catch (Exception ex)
+    {
+        error = $"YAML validation failed: {ex.Message}";
+        return false;
+    }
+}
+
+}

+ 14 - 0
Shared.Rcl/YamlFilePage.razor

@@ -0,0 +1,14 @@
+@page "/yaml"
+@using Shared.Rcl.Console
+<PageTitle>Cli</PageTitle>
+
+<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6">
+
+    <YamlFileComponent Path="config.yaml">
+            
+    </YamlFileComponent>
+</div>
+
+@code {
+
+}