From addf3758e41531e405b6640e0da2928c91f0e2fc Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 14 Jul 2025 14:27:40 +0200 Subject: [PATCH 1/8] API landing page now contains a listing of all API's for ease of reference --- .../Landing/LandingView.cshtml | 84 +++++++++- .../Operations/OperationView.cshtml | 5 +- .../Assets/api-docs.css | 150 +++++++++++++----- .../docs-builder/Http/DocumentationWebHost.cs | 3 + .../Http/ReloadableGeneratorState.cs | 2 + 5 files changed, 197 insertions(+), 47 deletions(-) diff --git a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml index 767bf996d..880d23948 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml +++ b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml @@ -1,10 +1,92 @@ @inherits RazorSliceHttpResult +@using Elastic.ApiExplorer.Landing +@using Elastic.ApiExplorer.Operations +@using Elastic.Documentation.Site.Navigation @implements IUsesLayout @functions { public GlobalLayoutViewModel LayoutModel => Model.CreateGlobalLayoutModel(); + + private IHtmlContent RenderOp(IReadOnlyCollection endpointOperations) + { + + + return HtmlString.Empty; + } + + private IHtmlContent RenderProduct(INavigationItem item) + { + if (item is INodeNavigationItem node) + { + foreach (var navigationItem in node.NavigationItems) + { + if (navigationItem is ClassificationNavigationItem classification) + { + +

@(classification.NavigationTitle)

+ + @RenderProduct(classification) + } + else if (navigationItem is TagNavigationItem tag) + { + +

@(tag.NavigationTitle)

+ + @RenderProduct(tag) + } + else if (navigationItem is EndpointNavigationItem endpoint) + { + var endpointOperations = + endpoint is { NavigationItems.Count: > 0 } && endpoint.NavigationItems.All(n => n.Hidden) + ? endpoint.NavigationItems + : []; + if (endpointOperations.Count > 0) + { + + @(endpoint.NavigationTitle) + @RenderOp(endpointOperations) + + } + else + { + @RenderProduct(endpoint) + } + } + else if (navigationItem is OperationNavigationItem operation) + { + + @(operation.NavigationTitle) + @RenderOp([operation]) + + } + else + { + throw new Exception($"Unexpected type: {item.GetType().FullName}"); + } + } + } + + return HtmlString.Empty; + + } }

@Model.ApiInfo.Title

@Model.ApiInfo.Description

-

License: @Model.ApiInfo.License?.Identifier

+

License: @Model.ApiInfo.License?.Name

+
+ + @RenderProduct(Model.CurrentNavigationItem.NavigationRoot) +
+
diff --git a/src/Elastic.ApiExplorer/Operations/OperationView.cshtml b/src/Elastic.ApiExplorer/Operations/OperationView.cshtml index a6e80d120..0c9977f23 100644 --- a/src/Elastic.ApiExplorer/Operations/OperationView.cshtml +++ b/src/Elastic.ApiExplorer/Operations/OperationView.cshtml @@ -7,10 +7,9 @@ public GlobalLayoutViewModel LayoutModel => Model.CreateGlobalLayoutModel(); } @{ - var parent = Model.CurrentNavigationItem.Parent as EndpointNavigationItem; var self = Model.CurrentNavigationItem as OperationNavigationItem; var allOperations = - parent is not null && parent.NavigationItems.Count > 0 && parent.NavigationItems.All(n => n.Hidden) + Model.CurrentNavigationItem.Parent is EndpointNavigationItem { NavigationItems.Count: > 0 } parent && parent.NavigationItems.All(n => n.Hidden) ? parent.NavigationItems : self is not null ? [self] @@ -87,4 +86,4 @@ - \ No newline at end of file + diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index 15bd423e8..f5ffcb737 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -42,6 +42,12 @@ @apply border-grey-30 text-grey-120 bg-white; font-weight: bold; } + a.api-url-list-item-landing { + font-weight: normal !important; + } + a.api-url-list-item-landing:hover { + font-weight: normal !important; + } } li:only-child { a.current { @@ -51,53 +57,111 @@ @apply border-grey-20 bg-white; } } - .api-method { - @apply rounded-sm border; - padding-left: var(--spacing); - padding-right: var(--spacing); - font-family: var( - --default-mono-font-family, - ui-monospace, - SFMono-Regular, - Menlo, - Monaco, - Consolas, - 'Liberation Mono', - 'Courier New', - monospace - ); - font-feature-settings: var( - --default-mono-font-feature-settings, - normal - ); - font-variation-settings: var( - --default-mono-font-variation-settings, - normal - ); - font-size: 0.8em; - display: inline-block; - font-weight: bold; - } - .api-method-get { - @apply border-blue-elastic-30 bg-blue-elastic-10 text-blue-elastic; - } - .api-method-put { - @apply border-yellow-30 bg-yellow-10 text-yellow-90; - } - .api-method-post { - @apply border-green-30 bg-green-10 text-green-90; - } - .api-method-delete { - @apply border-red-30 bg-red-10 text-red-90; - } - .api-url { - margin-left: calc(var(--spacing) * 2); - display: inline-block; - } .api-url-list-item { @apply mt-4; } } +.api-overview { + .api-url-listing { + @apply mt-1; + } + .api-url-list-item { + @apply mt-1; + } + li { + a { + @apply text-grey-80 inline-block w-full p-2 pr-2 pl-2 no-underline; + @apply border-white rounded-sm border; + } + a:hover { + @apply bg-grey-10; + @apply border-grey-20; + } + a.current { + @apply text-grey-80 inline-block w-full p-2 pr-2 pl-2 no-underline; + @apply border-white rounded-sm border; + } + a.current:hover { + @apply bg-grey-10; + @apply border-grey-20; + } + } + li:only-child { + a.current { + @apply text-grey-80 inline-block w-full p-2 pr-2 pl-2 no-underline; + @apply border-white rounded-sm border; + } + a.current:hover { + @apply bg-grey-10; + } + } + table { + td { + text-align: left; + vertical-align: top; + } + td:has(h2) { + } + td:has(h3) { + @apply border-b-grey-20 border-b-1 pb-2 mb-2; + } + td.api-name { + @apply pr-2; + text-align: left; + font-weight: bold; + } + tr:has(td.api-name) { + @apply border-b-grey-10 border-b-1; + } + tr:has(td.api-name) td { + @apply pt-4 mt-4 pb-4 mb-4; + } + } +} + +.api-method { + @apply rounded-sm border; + padding-left: var(--spacing); + padding-right: var(--spacing); + font-family: var( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + 'Liberation Mono', + 'Courier New', + monospace + ); + font-feature-settings: var( + --default-mono-font-feature-settings, + normal + ); + font-variation-settings: var( + --default-mono-font-variation-settings, + normal + ); + font-size: 0.8em; + display: inline-block; + font-weight: bold; +} +.api-method-get { + @apply border-blue-elastic-30 bg-blue-elastic-10 text-blue-elastic; +} +.api-method-put { + @apply border-yellow-30 bg-yellow-10 text-yellow-90; +} +.api-method-post { + @apply border-green-30 bg-green-10 text-green-90; +} +.api-method-delete { + @apply border-red-30 bg-red-10 text-red-90; +} +.api-url { + margin-left: calc(var(--spacing) * 2); + display: inline-block; +} #elastic-api-v3 { dt a { @apply no-underline; diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs index cbd856b70..307433250 100644 --- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs +++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs @@ -165,6 +165,9 @@ await context.Response.WriteAsync(@" private async Task ServeApiFile(ReloadableGeneratorState holder, string slug, Cancel ctx) { +#if DEBUG + await holder.ReloadApiReferences(ctx); +#endif var path = Path.Combine(holder.ApiPath.FullName, slug.Trim('/'), "index.html"); var info = _writeFileSystem.FileInfo.New(path); if (info.Exists) diff --git a/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs b/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs index 971cf6979..ef931880d 100644 --- a/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs +++ b/src/tooling/docs-builder/Http/ReloadableGeneratorState.cs @@ -40,6 +40,8 @@ public async Task ReloadAsync(Cancel ctx) await ReloadApiReferences(generator.MarkdownStringRenderer, ctx); } + public async Task ReloadApiReferences(Cancel ctx) => await ReloadApiReferences(_generator.MarkdownStringRenderer, ctx); + private async Task ReloadApiReferences(IMarkdownStringRenderer markdownStringRenderer, Cancel ctx) { if (ApiPath.Exists) From 234bd0d28badd2017043d19a96c78f150250c783 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 14 Jul 2025 16:21:29 +0200 Subject: [PATCH 2/8] simplify navigation if using tag groups --- .../Landing/LandingNavigationItem.cs | 4 +- .../Landing/LandingView.cshtml | 2 +- .../Navigation/_TocTree.cshtml | 86 +++++++++---------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs b/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs index 28ef25b87..9fb5c00ef 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs +++ b/src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs @@ -52,7 +52,7 @@ public LandingNavigationItem(string url) } /// - public bool IsUsingNavigationDropdown => NavigationItems.OfType().Any(); + public bool IsUsingNavigationDropdown => false; } public interface IApiGroupingNavigationItem : INodeNavigationItem @@ -106,7 +106,7 @@ public class ClassificationNavigationItem(ApiClassification classification, Land public override string Id { get; } = ShortId.Create(classification.Name); /// - public bool IsUsingNavigationDropdown => true; + public bool IsUsingNavigationDropdown => false; } public class TagNavigationItem(ApiTag tag, IRootNavigationItem rootNavigation, INodeNavigationItem parent) diff --git a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml index 880d23948..8d81b5502 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml +++ b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml @@ -82,7 +82,7 @@ }

@Model.ApiInfo.Title

-

@Model.ApiInfo.Description

+

@Model.RenderMarkdown(Model.ApiInfo.Description)

License: @Model.ApiInfo.License?.Name

diff --git a/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml b/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml index ac5386eb1..d54da09ea 100644 --- a/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml +++ b/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml @@ -6,44 +6,44 @@ var currentTopLevelItem = Model.TopLevelItems.FirstOrDefault(i => i.Id == Model.Tree.Id) ?? Model.Tree; } @if (Model.IsUsingNavigationDropdown && currentTopLevelItem is { Index: not null }) - { -
-
- + { +
+
+
@@ -57,16 +57,16 @@ @Model.Title } - -
    - @await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem - { - IsPrimaryNavEnabled = Model.IsPrimaryNavEnabled, - IsGlobalAssemblyBuild = Model.IsGlobalAssemblyBuild, - Level = 0, - SubTree = Model.Tree, - RootNavigationId = Model.Tree.Id, - MaxLevel = Model.MaxLevel - })) -
+ +
    + @await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem + { + IsPrimaryNavEnabled = Model.IsPrimaryNavEnabled, + IsGlobalAssemblyBuild = Model.IsGlobalAssemblyBuild, + Level = 0, + SubTree = Model.Tree, + RootNavigationId = Model.Tree.Id, + MaxLevel = Model.MaxLevel + })) +
From a48896fc17aad2321fa4f7dc2b76ffba73bb6739 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 14 Jul 2025 16:27:35 +0200 Subject: [PATCH 3/8] lint and only reload API on debug --- .../Assets/api-docs.css | 15 ++++++--------- .../docs-builder/Http/DocumentationWebHost.cs | 4 +++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index f5ffcb737..5e0bf16e7 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -71,7 +71,7 @@ li { a { @apply text-grey-80 inline-block w-full p-2 pr-2 pl-2 no-underline; - @apply border-white rounded-sm border; + @apply rounded-sm border border-white; } a:hover { @apply bg-grey-10; @@ -79,7 +79,7 @@ } a.current { @apply text-grey-80 inline-block w-full p-2 pr-2 pl-2 no-underline; - @apply border-white rounded-sm border; + @apply rounded-sm border border-white; } a.current:hover { @apply bg-grey-10; @@ -89,7 +89,7 @@ li:only-child { a.current { @apply text-grey-80 inline-block w-full p-2 pr-2 pl-2 no-underline; - @apply border-white rounded-sm border; + @apply rounded-sm border border-white; } a.current:hover { @apply bg-grey-10; @@ -103,7 +103,7 @@ td:has(h2) { } td:has(h3) { - @apply border-b-grey-20 border-b-1 pb-2 mb-2; + @apply border-b-grey-20 mb-2 border-b-1 pb-2; } td.api-name { @apply pr-2; @@ -114,7 +114,7 @@ @apply border-b-grey-10 border-b-1; } tr:has(td.api-name) td { - @apply pt-4 mt-4 pb-4 mb-4; + @apply mt-4 mb-4 pt-4 pb-4; } } } @@ -134,10 +134,7 @@ 'Courier New', monospace ); - font-feature-settings: var( - --default-mono-font-feature-settings, - normal - ); + font-feature-settings: var(--default-mono-font-feature-settings, normal); font-variation-settings: var( --default-mono-font-variation-settings, normal diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs index 307433250..cd93f081f 100644 --- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs +++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs @@ -166,7 +166,9 @@ await context.Response.WriteAsync(@" private async Task ServeApiFile(ReloadableGeneratorState holder, string slug, Cancel ctx) { #if DEBUG - await holder.ReloadApiReferences(ctx); + // only reload when actually debugging + if (System.Diagnostics.Debugger.IsAttached) + await holder.ReloadApiReferences(ctx); #endif var path = Path.Combine(holder.ApiPath.FullName, slug.Trim('/'), "index.html"); var info = _writeFileSystem.FileInfo.New(path); From 8f7c6d9aaf220c57e90b3f71997de5936f841d2c Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 14 Jul 2025 20:08:33 +0200 Subject: [PATCH 4/8] Allow multiple API source to be specified in a single docset --- docs/_docset.yml | 5 +- src/Elastic.ApiExplorer/OpenApiGenerator.cs | 84 ++++++++++--------- .../Operations/OperationNavigationItem.cs | 3 +- .../Operations/OperationView.cshtml | 2 +- .../Builder/ConfigurationFile.cs | 19 +++-- .../Elastic.ApiExplorer.Tests/ReaderTests.cs | 11 +-- 6 files changed, 70 insertions(+), 54 deletions(-) diff --git a/docs/_docset.yml b/docs/_docset.yml index 15273c6dd..098af0ba7 100644 --- a/docs/_docset.yml +++ b/docs/_docset.yml @@ -20,8 +20,9 @@ subs: features: primary-nav: false -#api: kibana-openapi.json -api: elasticsearch-openapi.json +api: + elasticsearch: elasticsearch-openapi.json + kibana: kibana-openapi.json toc: - file: index.md diff --git a/src/Elastic.ApiExplorer/OpenApiGenerator.cs b/src/Elastic.ApiExplorer/OpenApiGenerator.cs index cb08b4cde..fc0e61008 100644 --- a/src/Elastic.ApiExplorer/OpenApiGenerator.cs +++ b/src/Elastic.ApiExplorer/OpenApiGenerator.cs @@ -44,9 +44,9 @@ public class OpenApiGenerator(BuildContext context, IMarkdownStringRenderer mark private readonly IFileSystem _writeFileSystem = context.WriteFileSystem; private readonly StaticFileContentHashProvider _contentHashProvider = new(new EmbeddedOrPhysicalFileProvider(context)); - public LandingNavigationItem CreateNavigation(OpenApiDocument openApiDocument) + public LandingNavigationItem CreateNavigation(string apiUrlSuffix, OpenApiDocument openApiDocument) { - var url = $"{context.UrlPathPrefix}/api"; + var url = $"{context.UrlPathPrefix}/api/" + apiUrlSuffix; var rootNavigation = new LandingNavigationItem(url); var ops = openApiDocument.Paths @@ -163,20 +163,21 @@ group tagGroup by classificationGroup.Key var classificationNavigationItem = new ClassificationNavigationItem(classification, rootNavigation, rootNavigation); var tagNavigationItems = new List>(); - CreateTagNavigationItems(classification, classificationNavigationItem, classificationNavigationItem, tagNavigationItems); + CreateTagNavigationItems(apiUrlSuffix, classification, classificationNavigationItem, classificationNavigationItem, tagNavigationItems); topLevelNavigationItems.Add(classificationNavigationItem); // if there is only a single tag item will be added directly to the classificationNavigationItem, otherwise they will be added to the tagNavigationItems if (classificationNavigationItem.NavigationItems.Count == 0) classificationNavigationItem.NavigationItems = tagNavigationItems; } else - CreateTagNavigationItems(classification, rootNavigation, rootNavigation, topLevelNavigationItems); + CreateTagNavigationItems(apiUrlSuffix, classification, rootNavigation, rootNavigation, topLevelNavigationItems); } rootNavigation.NavigationItems = topLevelNavigationItems; return rootNavigation; } private void CreateTagNavigationItems( + string apiUrlSuffix, ApiClassification classification, IRootNavigationItem rootNavigation, IApiGroupingNavigationItem parent, @@ -190,13 +191,13 @@ List> parentNavig if (hasTags) { var tagNavigationItem = new TagNavigationItem(tag, rootNavigation, parent); - CreateEndpointNavigationItems(rootNavigation, tag, tagNavigationItem, endpointNavigationItems); + CreateEndpointNavigationItems(apiUrlSuffix, rootNavigation, tag, tagNavigationItem, endpointNavigationItems); parentNavigationItems.Add(tagNavigationItem); tagNavigationItem.NavigationItems = endpointNavigationItems; } else { - CreateEndpointNavigationItems(rootNavigation, tag, parent, endpointNavigationItems); + CreateEndpointNavigationItems(apiUrlSuffix, rootNavigation, tag, parent, endpointNavigationItems); if (parent is ClassificationNavigationItem classificationNavigationItem) classificationNavigationItem.NavigationItems = endpointNavigationItems; else if (parent is LandingNavigationItem landingNavigationItem) @@ -206,6 +207,7 @@ List> parentNavig } private void CreateEndpointNavigationItems( + string apiUrlSuffix, IRootNavigationItem rootNavigation, ApiTag tag, IApiGroupingNavigationItem parentNavigationItem, @@ -220,7 +222,7 @@ List endpointNavigationItems var operationNavigationItems = new List(); foreach (var operation in endpoint.Operations) { - var operationNavigationItem = new OperationNavigationItem(context.UrlPathPrefix, operation, rootNavigation, endpointNavigationItem) + var operationNavigationItem = new OperationNavigationItem(context.UrlPathPrefix, apiUrlSuffix, operation, rootNavigation, endpointNavigationItem) { Hidden = true }; @@ -232,7 +234,7 @@ List endpointNavigationItems else { var operation = endpoint.Operations.First(); - var operationNavigationItem = new OperationNavigationItem(context.UrlPathPrefix, operation, rootNavigation, parentNavigationItem); + var operationNavigationItem = new OperationNavigationItem(context.UrlPathPrefix, apiUrlSuffix, operation, rootNavigation, parentNavigationItem); endpointNavigationItems.Add(operationNavigationItem); } @@ -241,47 +243,51 @@ List endpointNavigationItems public async Task Generate(Cancel ctx = default) { - if (context.Configuration.OpenApiSpecification is null) + if (context.Configuration.OpenApiSpecifications is null) return; - var openApiDocument = await OpenApiReader.Create(context.Configuration.OpenApiSpecification); - if (openApiDocument is null) - return; - - var navigation = CreateNavigation(openApiDocument); - _logger.LogInformation("Generating OpenApiDocument {Title}", openApiDocument.Info.Title); - - var navigationRenderer = new IsolatedBuildNavigationHtmlWriter(context, navigation); + foreach (var (prefix, path) in context.Configuration.OpenApiSpecifications) + { + var openApiDocument = await OpenApiReader.Create(path); + if (openApiDocument is null) + return; + var navigation = CreateNavigation(prefix, openApiDocument); + _logger.LogInformation("Generating OpenApiDocument {Title}", openApiDocument.Info.Title); - var renderContext = new ApiRenderContext(context, openApiDocument, _contentHashProvider) - { - NavigationHtml = string.Empty, - CurrentNavigation = navigation, - MarkdownRenderer = markdownStringRenderer - }; - _ = await Render(navigation, navigation.Index, renderContext, navigationRenderer, ctx); - await RenderNavigationItems(navigation); + var navigationRenderer = new IsolatedBuildNavigationHtmlWriter(context, navigation); - async Task RenderNavigationItems(INavigationItem currentNavigation) - { - if (currentNavigation is INodeNavigationItem node) + var renderContext = new ApiRenderContext(context, openApiDocument, _contentHashProvider) { - _ = await Render(node, node.Index, renderContext, navigationRenderer, ctx); - foreach (var child in node.NavigationItems) - await RenderNavigationItems(child); - } + NavigationHtml = string.Empty, + CurrentNavigation = navigation, + MarkdownRenderer = markdownStringRenderer + }; + _ = await Render(prefix, navigation, navigation.Index, renderContext, navigationRenderer, ctx); + await RenderNavigationItems(navigation); + async Task RenderNavigationItems(INavigationItem currentNavigation) + { + if (currentNavigation is INodeNavigationItem node) + { + _ = await Render(prefix, node, node.Index, renderContext, navigationRenderer, ctx); + foreach (var child in node.NavigationItems) + await RenderNavigationItems(child); + } -#pragma warning disable IDE0045 - else if (currentNavigation is ILeafNavigationItem leaf) -#pragma warning restore IDE0045 - _ = await Render(leaf, leaf.Model, renderContext, navigationRenderer, ctx); - else - throw new Exception($"Unknown navigation item type {currentNavigation.GetType()}"); + else + { + _ = currentNavigation is ILeafNavigationItem leaf + ? await Render(prefix, leaf, leaf.Model, renderContext, navigationRenderer, ctx) + : throw new Exception($"Unknown navigation item type {currentNavigation.GetType()}"); + } + } } } - private async Task Render(INavigationItem current, T page, ApiRenderContext renderContext, IsolatedBuildNavigationHtmlWriter navigationRenderer, Cancel ctx) +#pragma warning disable IDE0060 + private async Task Render(string prefix, INavigationItem current, T page, ApiRenderContext renderContext, +#pragma warning restore IDE0060 + IsolatedBuildNavigationHtmlWriter navigationRenderer, Cancel ctx) where T : INavigationModel, IPageRenderer { var outputFile = OutputFile(current); diff --git a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs index 6e62145f7..972197329 100644 --- a/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs +++ b/src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs @@ -29,6 +29,7 @@ public class OperationNavigationItem : ILeafNavigationItem, IEndpo { public OperationNavigationItem( string? urlPathPrefix, + string apiUrlSuffix, ApiOperation apiOperation, IRootNavigationItem root, IApiGroupingNavigationItem parent @@ -39,7 +40,7 @@ IApiGroupingNavigationItem parent NavigationTitle = apiOperation.ApiName; Parent = parent; var moniker = apiOperation.Operation.OperationId ?? apiOperation.Route.Replace("}", "").Replace("{", "").Replace('/', '-'); - Url = $"{urlPathPrefix}/api/endpoints/{moniker}"; + Url = $"{urlPathPrefix?.TrimEnd('/')}/api/{apiUrlSuffix}/{moniker}"; Id = ShortId.Create(Url); } diff --git a/src/Elastic.ApiExplorer/Operations/OperationView.cshtml b/src/Elastic.ApiExplorer/Operations/OperationView.cshtml index 0c9977f23..26405b568 100644 --- a/src/Elastic.ApiExplorer/Operations/OperationView.cshtml +++ b/src/Elastic.ApiExplorer/Operations/OperationView.cshtml @@ -34,7 +34,7 @@ var method = overload.Model.OperationType.ToString().ToLowerInvariant(); var current = overload.Model.Route == Model.Operation.Route && overload.Model.OperationType == Model.Operation.OperationType ? "current" : "";
  • - + @method.ToUpperInvariant() @overload.Model.Route diff --git a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs index 47c2b320d..6598e7494 100644 --- a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs +++ b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs @@ -50,7 +50,7 @@ public record ConfigurationFile : ITableOfContentsScope public IDirectoryInfo ScopeDirectory { get; } - public IFileInfo? OpenApiSpecification { get; } + public IReadOnlyDictionary? OpenApiSpecifications { get; } /// This is a documentation set that is not linked to by assembler. /// Setting this to true relaxes a few restrictions such as mixing toc references with file and folder reference @@ -111,11 +111,18 @@ public ConfigurationFile(IDocumentationContext context) // read this later break; case "api": - var specification = reader.ReadString(entry.Entry); - if (specification is null) + var configuredApis = reader.ReadDictionary(entry.Entry); + if (configuredApis.Count == 0) break; - var path = Path.Combine(context.DocumentationSourceDirectory.FullName, specification); - OpenApiSpecification = context.ReadFileSystem.FileInfo.New(path); + + var specs = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var (k, v) in configuredApis) + { + var path = Path.Combine(context.DocumentationSourceDirectory.FullName, v); + var fi = context.ReadFileSystem.FileInfo.New(path); + specs[k] = fi; + } + OpenApiSpecifications = specs; break; case "products": if (entry.Entry.Value is not YamlSequenceNode sequence) @@ -129,7 +136,7 @@ public ConfigurationFile(IDocumentationContext context) YamlScalarNode? productId = null; foreach (var child in node.Children) { - if (child.Key is YamlScalarNode { Value: "id" } && child.Value is YamlScalarNode scalarNode) + if (child is { Key: YamlScalarNode { Value: "id" }, Value: YamlScalarNode scalarNode }) { productId = scalarNode; break; diff --git a/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs b/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs index f9c734b12..e8308c369 100644 --- a/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/ReaderTests.cs @@ -35,9 +35,9 @@ public async Task Reads() }; var context = new BuildContext(collector, new FileSystem(), versionsConfig); - context.Configuration.OpenApiSpecification.Should().NotBeNull(); + context.Configuration.OpenApiSpecifications.Should().NotBeNull().And.NotBeEmpty(); - var x = await OpenApiReader.Create(context.Configuration.OpenApiSpecification); + var x = await OpenApiReader.Create(context.Configuration.OpenApiSpecifications.First().Value); x.Should().NotBeNull(); x.BaseUri.Should().NotBeNull(); @@ -63,11 +63,12 @@ public async Task Navigation() var collector = new DiagnosticsCollector([]); var context = new BuildContext(collector, new FileSystem(), versionsConfig); var generator = new OpenApiGenerator(context, NoopMarkdownStringRenderer.Instance, NullLoggerFactory.Instance); - context.Configuration.OpenApiSpecification.Should().NotBeNull(); + context.Configuration.OpenApiSpecifications.Should().NotBeNull().And.NotBeEmpty(); - var openApiDocument = await OpenApiReader.Create(context.Configuration.OpenApiSpecification); + var (urlPathPrefix, fi) = context.Configuration.OpenApiSpecifications.First(); + var openApiDocument = await OpenApiReader.Create(fi); openApiDocument.Should().NotBeNull(); - var navigation = generator.CreateNavigation(openApiDocument); + var navigation = generator.CreateNavigation(urlPathPrefix, openApiDocument); navigation.Should().NotBeNull(); } From 95a37804bc608d797ff3c181505814f6dc327f72 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 23 Jul 2025 13:56:49 +0200 Subject: [PATCH 5/8] work on typename exposure --- Directory.Packages.props | 2 +- .../Operations/OperationView.cshtml | 62 ++++++++++++++++++- src/tooling/Directory.Build.props | 3 +- .../Http/ReloadGeneratorService.cs | 14 ++--- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index da5e2015f..6adc9123a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -42,7 +42,7 @@ - + diff --git a/src/Elastic.ApiExplorer/Operations/OperationView.cshtml b/src/Elastic.ApiExplorer/Operations/OperationView.cshtml index 26405b568..198641c48 100644 --- a/src/Elastic.ApiExplorer/Operations/OperationView.cshtml +++ b/src/Elastic.ApiExplorer/Operations/OperationView.cshtml @@ -1,10 +1,59 @@ @using Elastic.ApiExplorer.Landing @using Elastic.ApiExplorer.Operations @using Microsoft.OpenApi.Models +@using Microsoft.OpenApi.Models.Interfaces @inherits RazorSliceHttpResult @implements IUsesLayout @functions { public GlobalLayoutViewModel LayoutModel => Model.CreateGlobalLayoutModel(); + + public string GetTypeName(JsonSchemaType? type) + { + var typeName = ""; + if (type is null) + return "unknown and null"; + + if (type.Value.HasFlag(JsonSchemaType.Boolean)) + typeName = "boolean"; + else if (type.Value.HasFlag(JsonSchemaType.Integer)) + typeName = "integer"; + else if (type.Value.HasFlag(JsonSchemaType.String)) + typeName = "string"; + else if (type.Value.HasFlag(JsonSchemaType.Object)) + { + typeName = "object"; + } + else if (type.Value.HasFlag(JsonSchemaType.Null)) + typeName = "null"; + else if (type.Value.HasFlag(JsonSchemaType.Number)) + typeName = "number"; + else + { + } + + if (type.Value.HasFlag(JsonSchemaType.Array)) + typeName += " array"; + return typeName; + } + + public string GetTypeName(IOpenApiSchema propertyValue) + { + var typeName = string.Empty; + if (propertyValue.Type is not null) + { + typeName = GetTypeName(propertyValue.Type); + if (typeName is not "object" and not "array") + return typeName; + } + + if (propertyValue.Schema is not null) + return propertyValue.Schema; + + if (propertyValue.Enum is { Count: >0 } e) + return "enum"; + + return $"unknown value {typeName}"; + } } @{ var self = Model.CurrentNavigationItem as OperationNavigationItem; @@ -72,15 +121,26 @@ @if (operation.RequestBody is not null) {

    Request Body

    + var content = operation.RequestBody.Content.FirstOrDefault().Value; if (!string.IsNullOrEmpty(operation.RequestBody.Description)) {

    @operation.RequestBody.Description

    } + + if (content.Schema is not null) + {
    - @foreach (var path in operation.RequestBody.Content) + @foreach (var property in content.Schema.Properties) { + if (property.Value.Type is null) + { + + } +
    @property.Key @GetTypeName(property.Value)
    +
    @Model.RenderMarkdown(property.Value.Description)
    }
    + } }