Skip to content

Commit eda7f35

Browse files
Mpdreamzclaudecottielastic-observability-automation[bot]coderabbitai[bot]
authored
Surface deprecated prebuilt detection rules as a sibling nav page (#3167)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Felipe Cotti <felipe.cotti@elastic.co> Co-authored-by: elastic-observability-automation[bot] <180520183+elastic-observability-automation[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: snyk-bot <snyk-bot@snyk.io> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com> Co-authored-by: Fabrizio Ferri-Benedetti <fabri.ferribenedetti@elastic.co> Co-authored-by: Lisa Cawley <lcawley@elastic.co> Co-authored-by: shainaraskas <58563081+shainaraskas@users.noreply.github.com> Co-authored-by: Nassim Kammah <nkammah@gmail.com> Co-authored-by: Janeen Mikell Roberts <57149392+jmikell821@users.noreply.github.com> Co-authored-by: Taylor Swanson <90622908+taylor-swanson@users.noreply.github.com> Co-authored-by: Jan Calanog <jan.calanog@elastic.co> Co-authored-by: Martijn Laarman <ml@elastic.co> Signed-off-by: dependabot[bot] <support@github.com>
1 parent 8d81400 commit eda7f35

18 files changed

Lines changed: 637 additions & 95 deletions

File tree

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
<PackageVersion Include="MartinCostello.Logging.XUnit.v3" Version="0.7.1" />
5858
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
5959
<PackageVersion Include="Microsoft.OpenApi" Version="3.1.1" />
60+
<PackageVersion Include="Tomlyn" Version="2.3.2" />
6061
<PackageVersion Include="TUnit" Version="0.25.21" />
6162
<PackageVersion Include="xunit.v3.extensibility.core" Version="2.0.2" />
6263
<PackageVersion Include="WireMock.Net" Version="1.6.11" />

src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<PackageReference Include="NetEscapades.EnumGenerators" />
2222
<PackageReference Include="Nullean.Argh.Interfaces" />
2323
<PackageReference Include="Nullean.ScopedFileSystem" />
24-
<PackageReference Include="Samboy063.Tomlet" />
24+
<PackageReference Include="Tomlyn" />
2525
<PackageReference Include="Vecc.YamlDotNet.Analyzers.StaticGenerator" />
2626
<PackageReference Include="YamlDotNet" />
2727
</ItemGroup>

src/Elastic.Documentation.Configuration/Toc/DetectionRules/DetectionRuleOverviewRef.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,30 @@ public record DetectionRuleOverviewRef : FileRef
1010
{
1111
public IReadOnlyCollection<string> DetectionRuleFolders { get; }
1212

13+
/// <summary>Optional path to a markdown file whose content prefixes the deprecated rules listing page.</summary>
14+
public string? DeprecatedFile { get; init; }
15+
16+
/// <summary>
17+
/// The resolved deprecated-rules overview FileRef that should appear as a sibling to this ref in the nav.
18+
/// Set by <c>ResolveRuleOverviewReference</c> when a <c>_deprecated</c> subfolder is detected.
19+
/// </summary>
20+
public FileRef? DeprecatedSiblingRef { get; init; }
21+
1322
public DetectionRuleOverviewRef(
1423
string pathRelativeToDocumentationSet,
1524
string pathRelativeToContainer,
1625
IReadOnlyCollection<string> detectionRulesFolders,
1726
IReadOnlyCollection<ITableOfContentsItem> children,
18-
string context
27+
string context,
28+
string? deprecatedFile = null
1929
) : base(pathRelativeToDocumentationSet, pathRelativeToContainer, false, children, context)
2030
{
2131
PathRelativeToDocumentationSet = pathRelativeToDocumentationSet;
2232
PathRelativeToContainer = pathRelativeToContainer;
2333
DetectionRuleFolders = detectionRulesFolders;
2434
Children = children;
2535
Context = context;
36+
DeprecatedFile = deprecatedFile;
2637
}
2738

2839
public static IReadOnlyCollection<ITableOfContentsItem> CreateTableOfContentItems(IReadOnlyCollection<IDirectoryInfo> sourceFolders, string context, IDirectoryInfo baseDirectory)
@@ -38,6 +49,18 @@ public static IReadOnlyCollection<ITableOfContentsItem> CreateTableOfContentItem
3849
.ToArray();
3950
}
4051

52+
public static IReadOnlyCollection<ITableOfContentsItem> CreateDeprecatedTableOfContentItems(IReadOnlyCollection<IDirectoryInfo> sourceFolders, string context, IDirectoryInfo baseDirectory)
53+
{
54+
var tocItems = new List<ITableOfContentsItem>();
55+
foreach (var detectionRuleFolder in sourceFolders)
56+
{
57+
var children = ReadDeprecatedDetectionRuleFolder(detectionRuleFolder, context, baseDirectory);
58+
tocItems.AddRange(children);
59+
}
60+
61+
return tocItems.ToArray();
62+
}
63+
4164
private static IReadOnlyCollection<ITableOfContentsItem> ReadDetectionRuleFolder(IDirectoryInfo directory, string context, IDirectoryInfo baseDirectory)
4265
{
4366
IReadOnlyCollection<ITableOfContentsItem> children = directory
@@ -62,4 +85,25 @@ private static IReadOnlyCollection<ITableOfContentsItem> ReadDetectionRuleFolder
6285

6386
return children;
6487
}
88+
89+
private static IReadOnlyCollection<ITableOfContentsItem> ReadDeprecatedDetectionRuleFolder(IDirectoryInfo directory, string context, IDirectoryInfo baseDirectory)
90+
{
91+
IReadOnlyCollection<ITableOfContentsItem> children = directory
92+
.EnumerateFiles("*.*", SearchOption.AllDirectories)
93+
.Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden) && !f.Attributes.HasFlag(FileAttributes.System))
94+
.Where(f => !f.Directory!.Attributes.HasFlag(FileAttributes.Hidden) && !f.Directory!.Attributes.HasFlag(FileAttributes.System))
95+
// skip symlinks
96+
.Where(f => f.LinkTarget == null)
97+
.Where(f => f.Extension == ".toml")
98+
// only include files inside _deprecated subdirectories
99+
.Where(f => f.FullName.Contains($"{Path.DirectorySeparatorChar}_deprecated{Path.DirectorySeparatorChar}"))
100+
.Select(f =>
101+
{
102+
var relativePath = Path.GetRelativePath(baseDirectory.Parent!.FullName, f.FullName);
103+
return (ITableOfContentsItem)new DetectionRuleRef(f, relativePath, context);
104+
})
105+
.ToArray();
106+
107+
return children;
108+
}
65109
}

src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,12 @@ private static TableOfContents ResolveTableOfContents(
174174
};
175175

176176
if (resolvedItem != null)
177+
{
177178
resolved.Add(resolvedItem);
179+
// Emit the deprecated rules overview as a sibling immediately after the active rules ref
180+
if (resolvedItem is DetectionRuleOverviewRef { DeprecatedSiblingRef: { } deprecatedSibling })
181+
resolved.Add(deprecatedSibling);
182+
}
178183
}
179184

180185
return resolved;
@@ -453,9 +458,33 @@ private static ITableOfContentsItem ResolveRuleOverviewReference(IDiagnosticsCol
453458
.ToList();
454459
var tomlChildren = DetectionRuleOverviewRef.CreateTableOfContentItems(tocSourceFolders, context, baseDirectory);
455460

456-
var children = resolvedChildren.Concat(tomlChildren).ToList();
461+
var children = resolvedChildren.ToList();
462+
children.AddRange(tomlChildren);
457463

458-
return new DetectionRuleOverviewRef(fullPath, pathRelativeToContainer, detectionRuleRef.DetectionRuleFolders, children, context);
464+
// Auto-detect _deprecated subdirectories. When found, build the deprecated overview FileRef
465+
// and attach it as DeprecatedSiblingRef so ResolveTableOfContents can emit it as a sibling,
466+
// not as a child nested under the active rules.
467+
FileRef? deprecatedSiblingRef = null;
468+
var hasDeprecatedRules = tocSourceFolders.Any(d =>
469+
d.Exists && d.EnumerateDirectories("_deprecated", SearchOption.TopDirectoryOnly).Any());
470+
if (hasDeprecatedRules)
471+
{
472+
var deprecatedFileName = detectionRuleRef.DeprecatedFile ?? "deprecated-detection-rules.md";
473+
var overviewDir = fileSystem.Path.GetDirectoryName(fullPath);
474+
var deprecatedFullPath = string.IsNullOrEmpty(overviewDir)
475+
? deprecatedFileName
476+
: $"{overviewDir}/{deprecatedFileName}";
477+
var deprecatedPathRelativeToContainer = string.IsNullOrEmpty(containerPath)
478+
? deprecatedFullPath
479+
: deprecatedFullPath.Substring(containerPath.Length + 1);
480+
var deprecatedTomlChildren = DetectionRuleOverviewRef.CreateDeprecatedTableOfContentItems(tocSourceFolders, context, baseDirectory);
481+
deprecatedSiblingRef = new FileRef(deprecatedFullPath, deprecatedPathRelativeToContainer, false, deprecatedTomlChildren, context);
482+
}
483+
484+
return new DetectionRuleOverviewRef(fullPath, pathRelativeToContainer, detectionRuleRef.DetectionRuleFolders, children, context, detectionRuleRef.DeprecatedFile)
485+
{
486+
DeprecatedSiblingRef = deprecatedSiblingRef
487+
};
459488
}
460489

461490

src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public class TocItemYamlConverter : IYamlTypeConverter
7373
}
7474
value = childrenList;
7575
}
76-
else if (key.Value is "detection_rules" or "exclude")
76+
else if (key.Value is "detection_rules" or "exclude" or "deprecated_detection_rules")
7777
{
7878
// Parse the children list manually
7979
var childrenList = new List<string>();
@@ -144,11 +144,8 @@ public class TocItemYamlConverter : IYamlTypeConverter
144144
if (dictionary.TryGetValue("detection_rules", out var detectionRulesObj) && detectionRulesObj is string[] detectionRulesFolders &&
145145
dictionary.TryGetValue("file", out var detectionRulesFilePath) && detectionRulesFilePath is string detectionRulesFile)
146146
{
147-
// Create the index file reference (FolderIndexFileRef to mark it as the folder's index)
148-
// Store ONLY the file name - the folder path will be prepended during resolution
149-
// This allows validation to check if the file itself has deep paths
150-
// PathRelativeToContainer will be set during resolution
151-
return new DetectionRuleOverviewRef(detectionRulesFile, detectionRulesFile, detectionRulesFolders, children, placeholderContext);
147+
var deprecatedFile = dictionary.TryGetValue("deprecated_file", out var deprecatedFileObj) && deprecatedFileObj is string df ? df : null;
148+
return new DetectionRuleOverviewRef(detectionRulesFile, detectionRulesFile, detectionRulesFolders, children, placeholderContext, deprecatedFile);
152149
}
153150

154151
// Check for file reference (file: or hidden:)

src/Elastic.Documentation.Site/Assets/pages-nav.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,16 @@ export function initNav() {
134134

135135
// Normalize pathname by removing trailing slash to handle both URL variants
136136
const pathname = window.location.pathname.replace(/\/$/, '')
137+
138+
// When the page is a hidden nav item (e.g. an individual detection rule), the server
139+
// emits docs:nav-active pointing to the nearest visible ancestor so we can highlight it.
140+
const navActiveMeta = document.querySelector<HTMLMetaElement>(
141+
'meta[name="docs:nav-active"]'
142+
)
143+
const activePathname = navActiveMeta?.content ?? pathname
144+
137145
const navItems = $$(
138-
'a[href="' + pathname + '"], a[href="' + pathname + '/"]',
146+
'a[href="' + activePathname + '"], a[href="' + activePathname + '/"]',
139147
pagesNav
140148
)
141149
navItems.forEach((el) => {

src/Elastic.Documentation.Site/Layout/_Head.cshtml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
@await RenderPartialAsync(_Favicon.Create(Model))
4949
<meta name="robots" content="@(Model.AllowIndexing ? "index, follow" : "noindex, nofollow")">
5050
<meta name="htmx-config" content='{"selfRequestsOnly": true}'>
51+
@if (!string.IsNullOrEmpty(Model.NavigationActiveUrl))
52+
{
53+
<meta name="docs:nav-active" content="@Model.NavigationActiveUrl">
54+
}
5155
<meta property="og:type" content="website"/>
5256
<meta property="og:title" content="@Model.Title"/>
5357
<meta property="og:description" content="@Model.Description"/>

src/Elastic.Documentation.Site/_ViewModels.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ public record GlobalLayoutViewModel
4646
/// <summary>Breadcrumb trail for codex sub-header (Home / Group / Docset).</summary>
4747
public IReadOnlyList<CodexBreadcrumb>? CodexBreadcrumbs { get; init; }
4848

49+
/// <summary>
50+
/// When the current page is a hidden nav item (e.g. an individual detection rule page),
51+
/// the URL of its nearest visible ancestor. The client uses this to highlight the correct
52+
/// nav entry when the page has no rendered nav link of its own.
53+
/// </summary>
54+
public string? NavigationActiveUrl { get; init; }
55+
4956

5057
// Header properties for isolated mode
5158
public string? HeaderTitle { get; init; }

src/Elastic.Markdown/Elastic.Markdown.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
<ProjectReference Include="..\Elastic.Documentation.Svg\Elastic.Documentation.Svg.csproj" />
4848
</ItemGroup>
4949

50+
<ItemGroup>
51+
<InternalsVisibleTo Include="Elastic.Markdown.Tests"/>
52+
</ItemGroup>
53+
5054
<ItemGroup>
5155
<UpToDateCheckInput Remove="Myst\Directives\AppliesSwitch\AppliesItemView.cshtml" />
5256
<UpToDateCheckInput Remove="Myst\Directives\AppliesSwitch\AppliesSwitchView.cshtml" />

0 commit comments

Comments
 (0)