@using System.Collections.Specialized @using RackPeek.Domain.Persistence @using RackPeek.Domain.Persistence.Yaml @using RackPeek.Domain.Resources @using RackPeek.Domain.Resources.AccessPoints @using RackPeek.Domain.Resources.Desktops @using RackPeek.Domain.Resources.Firewalls @using RackPeek.Domain.Resources.Laptops @using RackPeek.Domain.Resources.Servers @using RackPeek.Domain.Resources.Switches @using RackPeek.Domain.Resources.SystemResources @using YamlDotNet.Serialization @using YamlDotNet.Serialization.NamingConventions @using Router = RackPeek.Domain.Resources.Routers.Router @inject ITextFileStore FileStore @inject IResourceCollection Resources
@Title
@if (!_isEditing) { } else { }
@if (!_exists) {
File does not exist.
} else if (_isEditing) { @if (!string.IsNullOrEmpty(_validationError)) {
@_validationError
} } else {
@_currentText
}
> Are you sure you want to delete @Path? @code { [Parameter][EditorRequired] public string Path { get; set; } = default!; [Parameter] public string Title { get; set; } = "Edit YAML"; [Parameter] public EventCallback 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("kind", new Dictionary { { 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.UpsUnits.Ups.KindLabel, typeof(RackPeek.Domain.Resources.UpsUnits.Ups) }, { SystemResource.KindLabel, typeof(SystemResource) }, { Service.KindLabel, typeof(Service) } }); }) .Build(); var root = deserializer.Deserialize(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(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; } } }