Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
99a0543
OpenAPI response and requests body support
Mpdreamz Jan 2, 2026
678d9e4
Ensure API explorer follows site layout
Mpdreamz Jan 4, 2026
60d4a92
touchups
Mpdreamz Jan 4, 2026
1df38fd
touchups
Mpdreamz Jan 4, 2026
e89273f
touchups
Mpdreamz Jan 5, 2026
2fac60f
Move to partials over view functions
Mpdreamz Jan 5, 2026
d0185ce
support finding collapsed properties in browser
Mpdreamz Jan 5, 2026
e83d455
Add support for deprecated, beta, and version badges in API docs
Mpdreamz Jan 5, 2026
1f9639c
Add validation constraints rendering for API documentation
Mpdreamz Jan 5, 2026
8d4acbb
Increase `MaxDepth` in `SchemaHelpers` from 4 to 100 for enhanced rec…
Mpdreamz Jan 5, 2026
6cd38c9
Add rendering for response headers in API documentation
Mpdreamz Jan 5, 2026
d0eb59d
Add support for server and security requirement rendering in API docs
Mpdreamz Jan 5, 2026
514c6f8
Add newline at the end of `RenderContext.cs` to comply with editorcon…
Mpdreamz Jan 5, 2026
6cd9491
refactor a tad more
Mpdreamz Jan 5, 2026
6665f53
refactor a tad more
Mpdreamz Jan 5, 2026
7a43798
touchups
Mpdreamz Jan 5, 2026
c119f65
touchups
Mpdreamz Jan 5, 2026
290f32c
touchups
Mpdreamz Jan 6, 2026
09c1729
Fix code quality issues from PR review
Mpdreamz Jan 6, 2026
ccb366e
format css js
Mpdreamz Jan 6, 2026
a7a4e0b
ensure we escape variables when rendering markdown
Mpdreamz Jan 6, 2026
d3052b6
address codeQL issues
Mpdreamz Jan 6, 2026
ab79a36
Potential fix for pull request finding 'Useless assignment to local v…
Mpdreamz Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore

.artifacts
.claude


# User-specific files
*.rsuser
Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<PackageVersion Include="InMemoryLogger" Version="1.0.66" />
<PackageVersion Include="MartinCostello.Logging.XUnit.v3" Version="0.6.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.OpenApi" Version="3.0.1" />
<PackageVersion Include="Microsoft.OpenApi" Version="3.1.1" />
<PackageVersion Include="TUnit" Version="0.25.21" />
<PackageVersion Include="xunit.v3.extensibility.core" Version="2.0.2" />
<PackageVersion Include="WireMock.Net" Version="1.6.11" />
Expand Down Expand Up @@ -111,4 +111,4 @@
</PackageVersion>
<PackageVersion Include="xunit.v3" Version="2.0.2" />
</ItemGroup>
</Project>
</Project>
33 changes: 28 additions & 5 deletions src/Elastic.ApiExplorer/ApiViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Text.RegularExpressions;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Configuration.Assembler;
Expand All @@ -11,23 +12,44 @@
using Elastic.Documentation.Site;
using Elastic.Documentation.Site.FileProviders;
using Microsoft.AspNetCore.Html;
using Microsoft.OpenApi;

namespace Elastic.ApiExplorer;

public abstract class ApiViewModel(ApiRenderContext context)
public record ApiTocItem(string Heading, string Slug, int Level = 2);

public record ApiLayoutViewModel : GlobalLayoutViewModel
{
public required IReadOnlyList<ApiTocItem> TocItems { get; init; }
}

public abstract partial class ApiViewModel(ApiRenderContext context)
{
public string NavigationHtml { get; } = context.NavigationHtml;
public StaticFileContentHashProvider StaticFileContentHashProvider { get; } = context.StaticFileContentHashProvider;
public INavigationItem CurrentNavigationItem { get; } = context.CurrentNavigation;
public IMarkdownStringRenderer MarkdownRenderer { get; } = context.MarkdownRenderer;
public BuildContext BuildContext { get; } = context.BuildContext;
public OpenApiDocument Document { get; } = context.Model;


public HtmlString RenderMarkdown(string? markdown)
{
if (string.IsNullOrEmpty(markdown))
return new HtmlString(string.Empty);

// Escape mustache-style patterns by wrapping in backticks (inline code won't process substitutions)
var escaped = MustachePattern().Replace(markdown, match => $"`{match.Value}`");
return new HtmlString(MarkdownRenderer.Render(escaped, null));
}

public HtmlString RenderMarkdown(string? markdown) =>
new(string.IsNullOrEmpty(markdown) ? string.Empty : MarkdownRenderer.Render(markdown, null));
// Regex to match mustache-style patterns like {{var}} or {{{var}}} that conflict with docs-builder substitutions
[GeneratedRegex(@"\{\{\{?[^}]+\}?\}\}")]
private static partial Regex MustachePattern();

protected virtual IReadOnlyList<ApiTocItem> GetTocItems() => [];

public GlobalLayoutViewModel CreateGlobalLayoutModel() =>
public ApiLayoutViewModel CreateGlobalLayoutModel() =>
new()
{
DocsBuilderVersion = ShortId.Create(BuildContext.Version),
Expand All @@ -43,6 +65,7 @@ public GlobalLayoutViewModel CreateGlobalLayoutModel() =>
CanonicalBaseUrl = BuildContext.CanonicalBaseUrl,
GoogleTagManager = new GoogleTagManagerConfiguration(),
Features = new FeatureFlags([]),
StaticFileContentHashProvider = StaticFileContentHashProvider
StaticFileContentHashProvider = StaticFileContentHashProvider,
TocItems = GetTocItems()
};
}
21 changes: 18 additions & 3 deletions src/Elastic.ApiExplorer/Landing/LandingView.cshtml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
@inherits RazorSliceHttpResult<Elastic.ApiExplorer.Landing.LandingViewModel>
@using Elastic.ApiExplorer.Landing
@using Elastic.ApiExplorer.Operations
@using Elastic.ApiExplorer.Schemas
@using Elastic.Documentation.Navigation
@using Elastic.Documentation.Site.Navigation
@implements IUsesLayout<Elastic.ApiExplorer._Layout, GlobalLayoutViewModel>
@implements IUsesLayout<Elastic.ApiExplorer._Layout, ApiLayoutViewModel>
@functions {
public GlobalLayoutViewModel LayoutModel => Model.CreateGlobalLayoutModel();
public ApiLayoutViewModel LayoutModel => Model.CreateGlobalLayoutModel();

private IHtmlContent RenderOp(IReadOnlyCollection<OperationNavigationItem> endpointOperations)
{
Expand Down Expand Up @@ -70,9 +71,23 @@
<td>@RenderOp([operation])</td>
</tr>
}
else if (navigationItem is SchemaCategoryNavigationItem schemaCategory)
{
<tr>
<td colspan="2"><h3>@(schemaCategory.NavigationTitle)</h3></td>
</tr>
@RenderProduct(schemaCategory)
}
else if (navigationItem is SchemaNavigationItem schemaItem)
{
<tr>
<td class="api-name"><a href="@schemaItem.Url">@(schemaItem.NavigationTitle)</a></td>
<td><code>@schemaItem.Model.SchemaId</code></td>
</tr>
}
else
{
throw new Exception($"Unexpected type: {item.GetType().FullName}");
throw new Exception($"Unexpected type: {navigationItem.GetType().FullName}");
}
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/Elastic.ApiExplorer/Layout/_ApiToc.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@inherits RazorSlice<Elastic.ApiExplorer.ApiTocItem[]>
<aside class="sidebar hidden lg:block w-full lg:max-w-65 px-6 lg:px-0">
<nav id="toc-nav" class="sidebar-nav lg:h-full">
<div class="pt-6">
@if (Model.Length > 0)
{
<div class="font-sans">
<div class="font-bold mb-2">On this page</div>
<div class="relative toc-progress-container font-body">
<div class="toc-progress-indicator absolute top-0 h-0 w-[1px] bg-blue-elastic transition-all duration-200 ease-out"></div>
<ul class="block w-full">
@foreach (var item in Model)
{
<li class="has-[:hover]:border-l-grey-80 items-center px-4 border-l-1 border-l-grey-20 has-[.current]:border-l-blue-elastic!">
<a class="sidebar-link inline-block my-1.5 @(item.Level == 3 ? "ml-4" : string.Empty)"
href="#@item.Slug">
@item.Heading
</a>
</li>
}
</ul>
</div>
</div>
}
</div>
</nav>
</aside>
79 changes: 79 additions & 0 deletions src/Elastic.ApiExplorer/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text.RegularExpressions;
using Elastic.ApiExplorer.Landing;
using Elastic.ApiExplorer.Operations;
using Elastic.ApiExplorer.Schemas;
using Elastic.Documentation;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Navigation;
Expand Down Expand Up @@ -174,6 +175,9 @@ group tagGroup by classificationGroup.Key
else
CreateTagNavigationItems(apiUrlSuffix, classification, rootNavigation, rootNavigation, topLevelNavigationItems);
}
// Add schema type pages for shared types
CreateSchemaNavigationItems(apiUrlSuffix, openApiDocument, rootNavigation, topLevelNavigationItems);

rootNavigation.NavigationItems = topLevelNavigationItems;
return rootNavigation;
}
Expand Down Expand Up @@ -317,6 +321,81 @@ IFileInfo OutputFile(INavigationItem currentNavigation)
}
}

private void CreateSchemaNavigationItems(
string apiUrlSuffix,
OpenApiDocument openApiDocument,
LandingNavigationItem rootNavigation,
List<IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem>> topLevelNavigationItems
)
{
var schemas = openApiDocument.Components?.Schemas;
if (schemas is null || schemas.Count == 0)
return;

var typesCategory = new SchemaCategory("Types", "Shared type definitions");
var typesCategoryNav = new SchemaCategoryNavigationItem(typesCategory, rootNavigation, rootNavigation);
var categoryNavigationItems = new List<IApiGroupingNavigationItem<IApiGroupingModel, INavigationItem>>();

// Query DSL - only show QueryContainer (individual queries are shown as properties within it)
var queryContainerSchema = schemas
.FirstOrDefault(s => s.Key == "_types.query_dsl.QueryContainer");

if (queryContainerSchema.Value is not null)
{
var queryCategory = new SchemaCategory("Query DSL", "Query type definitions");
var queryCategoryNav = new SchemaCategoryNavigationItem(queryCategory, rootNavigation, typesCategoryNav);
var apiSchema = new ApiSchema(queryContainerSchema.Key, "QueryContainer", "query-dsl", queryContainerSchema.Value);
var queryNavigationItems = new List<INavigationItem>
{
new SchemaNavigationItem(context.UrlPathPrefix, apiUrlSuffix, apiSchema, rootNavigation, queryCategoryNav)
};
queryCategoryNav.NavigationItems = queryNavigationItems;
categoryNavigationItems.Add(queryCategoryNav);
}

// Aggregations - only show AggregationContainer and Aggregate
var aggContainerSchema = schemas
.FirstOrDefault(s => s.Key == "_types.aggregations.AggregationContainer");

var aggregateSchema = schemas
.FirstOrDefault(s => s.Key == "_types.aggregations.Aggregate");

if (aggContainerSchema.Value is not null || aggregateSchema.Value is not null)
{
var aggCategory = new SchemaCategory("Aggregations", "Aggregation type definitions");
var aggCategoryNav = new SchemaCategoryNavigationItem(aggCategory, rootNavigation, typesCategoryNav);
var aggNavigationItems = new List<INavigationItem>();

if (aggContainerSchema.Value is not null)
{
var apiSchema = new ApiSchema(aggContainerSchema.Key, "AggregationContainer", "aggregations", aggContainerSchema.Value);
aggNavigationItems.Add(new SchemaNavigationItem(context.UrlPathPrefix, apiUrlSuffix, apiSchema, rootNavigation, aggCategoryNav));
}

if (aggregateSchema.Value is not null)
{
var apiSchema = new ApiSchema(aggregateSchema.Key, "Aggregate", "aggregations", aggregateSchema.Value);
aggNavigationItems.Add(new SchemaNavigationItem(context.UrlPathPrefix, apiUrlSuffix, apiSchema, rootNavigation, aggCategoryNav));
}

aggCategoryNav.NavigationItems = aggNavigationItems;
categoryNavigationItems.Add(aggCategoryNav);
}

if (categoryNavigationItems.Count > 0)
{
typesCategoryNav.NavigationItems = categoryNavigationItems;
topLevelNavigationItems.Add(typesCategoryNav);
}
}

private static string FormatSchemaDisplayName(string schemaId)
{
// Convert schema IDs like "_types.query_dsl.QueryContainer" to "QueryContainer"
var parts = schemaId.Split('.');
return parts.Length > 0 ? parts[^1] : schemaId;
}

private static string ClassifyElasticsearchTag(string tag)
{
#pragma warning disable IDE0066
Expand Down
Loading
Loading