VisualisePage.razor 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. @page "/visualise"
  2. @page "/visualise/{View}"
  3. @using RackPeek.Domain.Graph.Serialisers
  4. @using RackPeek.Domain.Graph.UseCases
  5. @using Shared.Rcl.Components.Graphs
  6. @inject BuildPhysicalTopologyUseCase TopologyUseCase
  7. @inject BuildLogicalGraphUseCase LogicalUseCase
  8. @inject NavigationManager Nav
  9. @inject IJSRuntime JS
  10. <PageTitle>Visualise</PageTitle>
  11. <div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6"
  12. data-testid="visualise-page-root">
  13. <div class="flex items-center justify-between mb-4">
  14. <div class="space-y-1">
  15. <div class="text-xs text-zinc-500 uppercase tracking-wider">Knowledge Base</div>
  16. <h1 class="text-lg text-zinc-100">
  17. Visualise
  18. <span class="text-zinc-500">:</span>
  19. <span class="text-emerald-400">@ActiveTitle</span>
  20. </h1>
  21. </div>
  22. <div class="flex items-center gap-4">
  23. <nav class="flex items-center gap-1 text-sm">
  24. <button class="@TabClass("topology")"
  25. data-testid="visualise-tab-topology"
  26. @onclick="@(() => SelectView("topology"))">
  27. Physical
  28. </button>
  29. <button class="@TabClass("logical")"
  30. data-testid="visualise-tab-logical"
  31. @onclick="@(() => SelectView("logical"))">
  32. Logical
  33. </button>
  34. </nav>
  35. <div class="flex items-center gap-1 text-sm">
  36. <button class="@ExportButtonClass"
  37. disabled="@CannotExport"
  38. data-testid="visualise-export-png"
  39. @onclick="ExportPngAsync">
  40. Save PNG
  41. </button>
  42. <button class="@ExportButtonClass"
  43. disabled="@CannotExport"
  44. data-testid="visualise-export-source"
  45. @onclick="ExportSourceAsync">
  46. Save Mermaid
  47. </button>
  48. </div>
  49. </div>
  50. </div>
  51. @if (_loading)
  52. {
  53. <div class="text-zinc-500 text-sm">loading inventory…</div>
  54. }
  55. else if (_error is not null)
  56. {
  57. <div class="text-red-400 text-sm">@_error</div>
  58. }
  59. else
  60. {
  61. <div class="border border-zinc-800 rounded-md bg-zinc-900/30" style="height: 75vh;">
  62. <GraphView Source="@_source"
  63. Id="@HostId"
  64. TestId="@($"visualise-graph-{ActiveView}")" />
  65. </div>
  66. }
  67. </div>
  68. @code {
  69. [Parameter] public string? View { get; set; }
  70. private string ActiveView => string.Equals(View, "logical", StringComparison.OrdinalIgnoreCase)
  71. ? "logical"
  72. : "topology";
  73. private string ActiveTitle => ActiveView switch
  74. {
  75. "logical" => "Logical — services & systems",
  76. _ => "Physical — hardware topology"
  77. };
  78. private string? _source;
  79. private bool _loading = true;
  80. private string? _error;
  81. private string? _lastLoadedView;
  82. private readonly MermaidSerialiser _serialiser = new();
  83. protected override async Task OnParametersSetAsync()
  84. {
  85. if (_lastLoadedView == ActiveView) return;
  86. _lastLoadedView = ActiveView;
  87. await LoadAsync();
  88. }
  89. private async Task LoadAsync()
  90. {
  91. _loading = true;
  92. _error = null;
  93. StateHasChanged();
  94. try
  95. {
  96. RackPeek.Domain.Graph.Graph graph = ActiveView switch
  97. {
  98. "logical" => await LogicalUseCase.ExecuteAsync(),
  99. _ => await TopologyUseCase.ExecuteAsync()
  100. };
  101. _source = _serialiser.Serialise(graph);
  102. }
  103. catch (Exception ex)
  104. {
  105. _error = $"Failed to build graph: {ex.Message}";
  106. _source = null;
  107. }
  108. finally
  109. {
  110. _loading = false;
  111. StateHasChanged();
  112. }
  113. }
  114. private void SelectView(string view)
  115. {
  116. Nav.NavigateTo($"visualise/{view}");
  117. }
  118. private const string HostId = "visualise-graph-host";
  119. private bool CannotExport => _loading || _source is null;
  120. private string ExportButtonClass =>
  121. "px-3 py-1 rounded border border-zinc-800 text-zinc-400 " +
  122. "hover:text-emerald-400 hover:border-zinc-700 disabled:opacity-40 disabled:hover:text-zinc-400";
  123. private string ExportFilenameBase =>
  124. ActiveView == "logical" ? "rackpeek-logical" : "rackpeek-topology";
  125. private async Task ExportPngAsync()
  126. {
  127. if (CannotExport) return;
  128. await JS.InvokeVoidAsync(
  129. "rackpeekGraph.downloadPng", HostId, $"{ExportFilenameBase}.png", ExportBackground, 2);
  130. }
  131. private const string ExportBackground = "#09090b";
  132. private async Task ExportSourceAsync()
  133. {
  134. if (CannotExport || _source is null) return;
  135. await JS.InvokeVoidAsync(
  136. "rackpeekGraph.downloadText", _source, $"{ExportFilenameBase}.mmd", "text/plain");
  137. }
  138. private string TabClass(string view) =>
  139. ActiveView == view
  140. ? "px-3 py-1 rounded border border-emerald-500/40 bg-emerald-500/10 text-emerald-400"
  141. : "px-3 py-1 rounded border border-zinc-800 text-zinc-400 hover:text-emerald-400 hover:border-zinc-700";
  142. }