@page "/visualise"
@page "/visualise/{View}"
@using RackPeek.Domain.Graph.Serialisers
@using RackPeek.Domain.Graph.UseCases
@using Shared.Rcl.Components.Graphs
@inject BuildPhysicalTopologyUseCase TopologyUseCase
@inject BuildLogicalGraphUseCase LogicalUseCase
@inject NavigationManager Nav
@inject IJSRuntime JS
Visualise
Knowledge Base
Visualise
:
@ActiveTitle
@if (_loading)
{
loading inventory…
}
else if (_error is not null)
{
@_error
}
else
{
}
@code {
[Parameter] public string? View { get; set; }
private string ActiveView => string.Equals(View, "logical", StringComparison.OrdinalIgnoreCase)
? "logical"
: "topology";
private string ActiveTitle => ActiveView switch
{
"logical" => "Logical — services & systems",
_ => "Physical — hardware topology"
};
private string? _source;
private bool _loading = true;
private string? _error;
private string? _lastLoadedView;
private readonly MermaidSerialiser _serialiser = new();
protected override async Task OnParametersSetAsync()
{
if (_lastLoadedView == ActiveView) return;
_lastLoadedView = ActiveView;
await LoadAsync();
}
private async Task LoadAsync()
{
_loading = true;
_error = null;
StateHasChanged();
try
{
RackPeek.Domain.Graph.Graph graph = ActiveView switch
{
"logical" => await LogicalUseCase.ExecuteAsync(),
_ => await TopologyUseCase.ExecuteAsync()
};
_source = _serialiser.Serialise(graph);
}
catch (Exception ex)
{
_error = $"Failed to build graph: {ex.Message}";
_source = null;
}
finally
{
_loading = false;
StateHasChanged();
}
}
private void SelectView(string view)
{
Nav.NavigateTo($"visualise/{view}");
}
private const string HostId = "visualise-graph-host";
private bool CannotExport => _loading || _source is null;
private string ExportButtonClass =>
"px-3 py-1 rounded border border-zinc-800 text-zinc-400 " +
"hover:text-emerald-400 hover:border-zinc-700 disabled:opacity-40 disabled:hover:text-zinc-400";
private string ExportFilenameBase =>
ActiveView == "logical" ? "rackpeek-logical" : "rackpeek-topology";
private async Task ExportPngAsync()
{
if (CannotExport) return;
await JS.InvokeVoidAsync(
"rackpeekGraph.downloadPng", HostId, $"{ExportFilenameBase}.png", ExportBackground, 2);
}
private const string ExportBackground = "#09090b";
private async Task ExportSourceAsync()
{
if (CannotExport || _source is null) return;
await JS.InvokeVoidAsync(
"rackpeekGraph.downloadText", _source, $"{ExportFilenameBase}.mmd", "text/plain");
}
private string TabClass(string view) =>
ActiveView == view
? "px-3 py-1 rounded border border-emerald-500/40 bg-emerald-500/10 text-emerald-400"
: "px-3 py-1 rounded border border-zinc-800 text-zinc-400 hover:text-emerald-400 hover:border-zinc-700";
}