diff --git a/docs/installation.md b/docs/installation.md
index 4264f46..930a616 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -179,3 +179,7 @@ Begge programmer understøtter systemd (`Type=notify`). Windows-login på Linux
## Opdatering
Se [opdatering.md](opdatering.md): regelpakken opdateres automatisk, og nye versioner kan installeres med `SQLInsight.Agent update apply` eller automatisk af en planlagt opgave. Manuelt: stop tjenesten (eller app poolen), kopiér de nye filer ind uden at overskrive `appsettings.json`, og start igen. Agenten opgraderer repository-skemaet selv.
+
+## Menuen i webbrugerfladen
+
+Menuen er delt i grupper, der kan foldes ud og sammen: **Overvågning**, **Analyse**, **Konfiguration og sikkerhed**, **Rapporter**, **Dynamics AX** (kun når en database er genkendt som AX), **Support**, **System** og **Community**. Gruppen med den side, man står på, er åben. Sider, der findes pr. instans (fx Analyse af tidsrum og Konfiguration), åbner et valg af instans; er der kun én instans, går de direkte til den. På en smal skærm (mobil) folder hele menuen sig sammen bag knappen **Menu**. Alt virker uden JavaScript.
diff --git a/src/SQLInsight.Repository/InsightRepository.Read.cs b/src/SQLInsight.Repository/InsightRepository.Read.cs
index 44abb5e..9e780ac 100644
--- a/src/SQLInsight.Repository/InsightRepository.Read.cs
+++ b/src/SQLInsight.Repository/InsightRepository.Read.cs
@@ -271,3 +271,14 @@ public sealed partial class InsightRepository
return rows.Select(r => new HourlyValue(r.Metric, DateTime.SpecifyKind(r.Hour, DateTimeKind.Utc), r.Value)).ToList();
}
}
+
+public sealed partial class InsightRepository
+{
+ /// Whether any instance has a database recognised as Dynamics AX (the AX menu is shown only then).
+ public async Task HasDynamicsAxDatabasesAsync(CancellationToken cancellationToken = default)
+ {
+ await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false);
+ return await connection.ExecuteScalarAsync(
+ "SELECT COUNT(*) FROM insight.Snapshots WHERE Kind = N'dynamics-ax' AND Json LIKE N'%\"database\"%';").ConfigureAwait(false) > 0;
+ }
+}
diff --git a/src/SQLInsight.Web/Components/Layout/MainLayout.razor b/src/SQLInsight.Web/Components/Layout/MainLayout.razor
index bdbbb0e..abdde83 100644
--- a/src/SQLInsight.Web/Components/Layout/MainLayout.razor
+++ b/src/SQLInsight.Web/Components/Layout/MainLayout.razor
@@ -9,19 +9,10 @@
SQLInsight Pro
-
+ @* On narrow screens the menu folds away behind "Menu" (a checkbox: no script). *@
+
+
+
diff --git a/src/SQLInsight.Web/Components/Layout/MainNav.razor b/src/SQLInsight.Web/Components/Layout/MainNav.razor
new file mode 100644
index 0000000..b4b3e64
--- /dev/null
+++ b/src/SQLInsight.Web/Components/Layout/MainNav.razor
@@ -0,0 +1,92 @@
+@inject NavigationManager Navigation
+@inject Microsoft.Extensions.Options.IOptions Options
+@inject InsightRepository Repository
+
+@* The menu in foldable groups (details/summary: no script needed). The group with the current page is open. *@
+
+
+@code {
+ public sealed record NavItem(string Title, string Href, params string[] Also);
+
+ public sealed record NavGroup(string Key, string Title, string Icon, IReadOnlyList Items);
+
+ /// The menu. Later pages go into the group they belong to.
+ public static readonly IReadOnlyList Groups =
+ [
+ new("overvaagning", "Overvågning", "servers",
+ [
+ new("Overblik", "/"),
+ new("Alle fund", "/fund", "/fund/"),
+ ]),
+ new("analyse", "Analyse", "chart",
+ [
+ new("Analyse af tidsrum", "/vaelg/analyse", "/analyse", "/sql"),
+ new("Top-SQL", "/vaelg/topsql"),
+ new("Ventetyper", "/vaelg/ventetyper", "/ventetyper/"),
+ new("Diskhastighed", "/vaelg/disk"),
+ ]),
+ new("sikkerhed", "Konfiguration og sikkerhed", "shield",
+ [
+ new("Konfiguration", "/vaelg/konfiguration", "/konfiguration"),
+ new("Rettigheder", "/rettigheder", "/rettigheder"),
+ ]),
+ new("rapporter", "Rapporter", "book",
+ [
+ new("Rapporter", "/rapporter"),
+ new("Revisionsrapport", "/rapporter#rettighedsrevision"),
+ ]),
+ new("ax", "Dynamics AX", "cube", []),
+ ];
+
+ private string _path = "/";
+ private bool _dynamicsAx;
+
+ protected override async Task OnInitializedAsync()
+ {
+ _path = "/" + Navigation.ToBaseRelativePath(Navigation.Uri).Split('?', '#')[0];
+ _dynamicsAx = Groups.Single(g => g.Key == "ax").Items.Count > 0 && await Repository.HasDynamicsAxDatabasesAsync();
+ }
+
+ /// The group holding the current page; Overvågning on the front page and the instance pages.
+ private bool IsOpen(NavGroup group) =>
+ group.Items.Any(i => Matches(i)) || group.Key == "overvaagning" && !Groups.Any(g => g.Key != "overvaagning" && g.Items.Any(Matches));
+
+ private bool Matches(NavItem item)
+ {
+ var href = item.Href.Split('?', '#')[0];
+ if (href == "/" ? _path == "/" : _path.StartsWith(href, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ // Pages under /instanser/{name}/… belong to the group of the menu entry for that kind of page.
+ return item.Also.Any(a => a.StartsWith('/') && _path.StartsWith("/instanser/", StringComparison.OrdinalIgnoreCase)
+ ? _path.Contains(a, StringComparison.OrdinalIgnoreCase)
+ : _path.StartsWith(a, StringComparison.OrdinalIgnoreCase));
+ }
+}
diff --git a/src/SQLInsight.Web/Components/Pages/Analysis.razor b/src/SQLInsight.Web/Components/Pages/Analysis.razor
index 4a2df2d..f57fe85 100644
--- a/src/SQLInsight.Web/Components/Pages/Analysis.razor
+++ b/src/SQLInsight.Web/Components/Pages/Analysis.razor
@@ -91,7 +91,7 @@
-
Tungeste forespørgsler
summeret over tidsrummet; teksten bliver her og sendes aldrig til SQLInsight Hub
+
Tungeste forespørgsler
summeret over tidsrummet; teksten bliver her og sendes aldrig til SQLInsight Hub
@if (_analysis.TopQueries.Count == 0)
{
Ingen forespørgselsstatistik i tidsrummet. Den indsamles hvert 5. minut fra version 1.0.3 og gemmes i @_analysis.RawDays dage.
diff --git a/src/SQLInsight.Web/Components/Pages/ChooseInstance.razor b/src/SQLInsight.Web/Components/Pages/ChooseInstance.razor
new file mode 100644
index 0000000..09d075f
--- /dev/null
+++ b/src/SQLInsight.Web/Components/Pages/ChooseInstance.razor
@@ -0,0 +1,69 @@
+@page "/vaelg/{Side}"
+@inject InsightRepository Repository
+@inject NavigationManager Navigation
+
+@* Menu entries for pages that exist per instance lead here: pick the instance (or go straight there with only one). *@
+@if (_target is null)
+{
+ Ukendt side · SQLInsight Pro
+
Ukendt side
+
Siden @Side findes ikke.
+ return;
+}
+
+@_target.Value.Title · SQLInsight Pro
+
@_target.Value.Title
+
Vælg instans.
+
+@if (_instances.Count == 0)
+{
+
Der er endnu ingen instanser. Tjek, at SQLInsight Agent kører og har instanser under SQLInsight:Instances.
+}
+else
+{
+
+
+
Instans
Server
+
+ @foreach (var i in _instances.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase))
+ {
+
Hvem har sysadmin og de øvrige forhøjede serverrettigheder nu (Windows-grupper foldet ud til personer), hvem der har fået eller mistet dem i perioden og af hvem, sa-kontoen, SQL-logins uden adgangskodepolitik og default trace. Indeholder navne: rapporten dannes her og sendes ikke nogen steder hen.