Skip to content

Simplify navigation interfaces and make them partially generic #1347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/Elastic.ApiExplorer/ApiRenderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ StaticFileContentHashProvider StaticFileContentHashProvider
: RenderContext<OpenApiDocument>(BuildContext, Model)
{
public required string NavigationHtml { get; init; }
public required INavigationItem CurrentNavigation { get; init; }
}
2 changes: 2 additions & 0 deletions src/Elastic.ApiExplorer/ApiViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
// See the LICENSE file in the project root for more information

using Elastic.Documentation.Site.FileProviders;
using Elastic.Documentation.Site.Navigation;

namespace Elastic.ApiExplorer;

public abstract class ApiViewModel
{
public required string NavigationHtml { get; init; }
public required StaticFileContentHashProvider StaticFileContentHashProvider { get; init; }
public required INavigationItem CurrentNavigationItem { get; init; }
}
47 changes: 22 additions & 25 deletions src/Elastic.ApiExplorer/Endpoints/ApiEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,64 @@

using System.IO.Abstractions;
using Elastic.ApiExplorer.Landing;
using Elastic.ApiExplorer.Operations;
using Elastic.Documentation.Site.Navigation;
using Microsoft.OpenApi.Models.Interfaces;
using RazorSlices;

namespace Elastic.ApiExplorer.Endpoints;

public record ApiEndpoint : IPageInformation, IPageRenderer<ApiRenderContext>
public record ApiEndpoint : INavigationModel, IPageRenderer<ApiRenderContext>
{
public ApiEndpoint(string url, string route, IOpenApiPathItem pathValue, IGroupNavigationItem navigationRoot)
public ApiEndpoint(string route, IOpenApiPathItem openApiPath)
{
Route = route;
PathValue = pathValue;
NavigationRoot = navigationRoot;
OpenApiPath = openApiPath;

//TODO
NavigationTitle = pathValue.Summary;
CrossLink = pathValue.Summary;
Url = url;
}

public string NavigationTitle { get; }
public string CrossLink { get; }
public string Url { get; }
public string Route { get; }
public IOpenApiPathItem PathValue { get; }
public IGroupNavigationItem NavigationRoot { get; }
public IOpenApiPathItem OpenApiPath { get; }

public async Task RenderAsync(FileSystemStream stream, ApiRenderContext context, Cancel ctx = default)
{
var viewModel = new IndexViewModel
{
ApiEndpoint = this,
StaticFileContentHashProvider = context.StaticFileContentHashProvider,
NavigationHtml = context.NavigationHtml
NavigationHtml = context.NavigationHtml,
CurrentNavigationItem = context.CurrentNavigation

};
var slice = EndpointView.Create(viewModel);
await slice.RenderAsync(stream, cancellationToken: ctx);
}
}

public class EndpointNavigationItem : IGroupNavigationItem
public class EndpointNavigationItem : INodeNavigationItem<ApiEndpoint, OperationNavigationItem>
{
public EndpointNavigationItem(int depth, ApiEndpoint apiEndpoint, IGroupNavigationItem? parent, LandingNavigationItem root)
public EndpointNavigationItem(int depth, string url, ApiEndpoint apiEndpoint, LandingNavigationItem parent, LandingNavigationItem root)
{
Parent = parent;
Depth = depth;
//Current = group.Current;
NavigationRoot = root;
Id = NavigationRoot.Id;

Index = apiEndpoint;
Current = apiEndpoint;
Endpoint = apiEndpoint;
Url = url;
//TODO
NavigationTitle = apiEndpoint.OpenApiPath.Summary;
}

public IGroupNavigationItem NavigationRoot { get; }
public string Id { get; }
public IGroupNavigationItem? Parent { get; set; }
public int Depth { get; }
public IPageInformation? Current { get; }
public IPageInformation? Index { get; }
public ApiEndpoint Endpoint { get; }
public IReadOnlyCollection<INavigationItem> NavigationItems { get; set; } = [];
public ApiEndpoint Index { get; }
public string Url { get; }
public string NavigationTitle { get; }

public IReadOnlyCollection<OperationNavigationItem> NavigationItems { get; set; } = [];

public INodeNavigationItem<INavigationModel, INavigationItem> NavigationRoot { get; }

public INodeNavigationItem<INavigationModel, INavigationItem>? Parent { get; set; }
}
4 changes: 2 additions & 2 deletions src/Elastic.ApiExplorer/Endpoints/EndpointView.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Description = "",
Layout = null,
PageTocItems = [],
CurrentDocument = Model.ApiEndpoint,
CurrentNavigationItem = Model.CurrentNavigationItem,
Previous = null,
Next = null,
NavigationHtml = Model.NavigationHtml,
Expand All @@ -29,5 +29,5 @@
};
}
<section id="elastic-docs-v3">
<h1>@Model.ApiEndpoint.Url</h1>
<h1>@Model.CurrentNavigationItem.Url</h1>
</section>
34 changes: 15 additions & 19 deletions src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,15 @@
// See the LICENSE file in the project root for more information

using System.IO.Abstractions;
using Elastic.ApiExplorer.Endpoints;
using Elastic.Documentation.Extensions;
using Elastic.Documentation.Site.Navigation;
using RazorSlices;

namespace Elastic.ApiExplorer.Landing;

public class ApiLanding(IGroupNavigationItem root, string url) : IPageInformation, IPageRenderer<ApiRenderContext>
public class ApiLanding : INavigationModel, IPageRenderer<ApiRenderContext>
{
public IGroupNavigationItem NavigationRoot { get; } = root;
public string Url { get; } = url;

//TODO
public string NavigationTitle { get; } = "API Documentation";
public string CrossLink { get; } = string.Empty;

public async Task RenderAsync(FileSystemStream stream, ApiRenderContext context, Cancel ctx = default)
{
var viewModel = new LandingViewModel
Expand All @@ -26,34 +20,36 @@ public async Task RenderAsync(FileSystemStream stream, ApiRenderContext context,
StaticFileContentHashProvider = context.StaticFileContentHashProvider,
NavigationHtml = context.NavigationHtml,
ApiInfo = context.Model.Info,
CurrentNavigationItem = context.CurrentNavigation
};
var slice = LandingView.Create(viewModel);
await slice.RenderAsync(stream, cancellationToken: ctx);
}
}

public class LandingNavigationItem : IGroupNavigationItem
public class LandingNavigationItem : INodeNavigationItem<ApiLanding, EndpointNavigationItem>
{
public IGroupNavigationItem NavigationRoot { get; }
public INodeNavigationItem<INavigationModel, INavigationItem> NavigationRoot { get; }
public string Id { get; }
public IGroupNavigationItem? Parent { get; set; }
public int Depth { get; }
public IPageInformation Current { get; set; }
public IPageInformation Index { get; set; }
public ApiLanding Landing { get; set; }
public IReadOnlyCollection<INavigationItem> NavigationItems { get; set; } = [];
public ApiLanding Index { get; }
public INodeNavigationItem<INavigationModel, INavigationItem>? Parent { get; set; }
public IReadOnlyCollection<EndpointNavigationItem> NavigationItems { get; set; } = [];
public string Url { get; }

//TODO
public string NavigationTitle { get; } = "API Documentation";


public LandingNavigationItem(string url)
{
Parent = null;
Depth = 0;
NavigationRoot = this;
Id = ShortId.Create("root");

var landing = new ApiLanding(this, url);
var landing = new ApiLanding();
Url = url;

Index = landing;
Current = landing;
Landing = landing;
}
}
2 changes: 1 addition & 1 deletion src/Elastic.ApiExplorer/Landing/LandingView.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Description = "",
Layout = null,
PageTocItems = [],
CurrentDocument = Model.Landing,
CurrentNavigationItem = Model.CurrentNavigationItem,
Previous = null,
Next = null,
NavigationHtml = Model.NavigationHtml,
Expand Down
29 changes: 15 additions & 14 deletions src/Elastic.ApiExplorer/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ public static LandingNavigationItem CreateNavigation(OpenApiDocument openApiDocu
foreach (var path in openApiDocument.Paths)
{
var endpointUrl = $"{url}/{path.Key.Trim('/').Replace('/', '-').Replace("{", "").Replace("}", "")}";
var apiEndpoint = new ApiEndpoint(endpointUrl, path.Key, path.Value, rootNavigation);
var endpointNavigationItem = new EndpointNavigationItem(1, apiEndpoint, rootNavigation, rootNavigation);
var apiEndpoint = new ApiEndpoint(path.Key, path.Value);
var endpointNavigationItem = new EndpointNavigationItem(1, endpointUrl, apiEndpoint, rootNavigation, rootNavigation);
var endpointNavigationItems = new List<OperationNavigationItem>();
foreach (var operation in path.Value.Operations)
{
var operationUrl = $"{endpointUrl}/{operation.Key.ToString().ToLowerInvariant()}";
var apiOperation = new ApiOperation(operationUrl, operation.Key, operation.Value, rootNavigation);
var navigation = new OperationNavigationItem(2, apiOperation, endpointNavigationItem, rootNavigation);
var apiOperation = new ApiOperation(operation.Key, operation.Value);
var navigation = new OperationNavigationItem(2, operationUrl, apiOperation, endpointNavigationItem, rootNavigation);
endpointNavigationItems.Add(navigation);
}

Expand Down Expand Up @@ -65,32 +65,33 @@ public async Task Generate(Cancel ctx = default)

var renderContext = new ApiRenderContext(context, openApiDocument, _contentHashProvider)
{
NavigationHtml = navigationHtml
NavigationHtml = navigationHtml,
CurrentNavigation = navigation,
};
_ = await Render(navigation.Landing, renderContext, ctx);
foreach (var endpoint in navigation.NavigationItems.OfType<EndpointNavigationItem>())
_ = await Render(navigation.Index, renderContext, ctx);
foreach (var endpoint in navigation.NavigationItems)
{
_ = await Render(endpoint.Endpoint, renderContext, ctx);
foreach (var operation in endpoint.NavigationItems.OfType<OperationNavigationItem>())
_ = await Render(operation.Operation, renderContext, ctx);
_ = await Render(endpoint.Index, renderContext, ctx);
foreach (var operation in endpoint.NavigationItems)
_ = await Render(operation.Model, renderContext, ctx);
}
}

private async Task<IFileInfo> Render<T>(T page, ApiRenderContext renderContext, Cancel ctx)
where T : IPageInformation, IPageRenderer<ApiRenderContext>
where T : INavigationModel, IPageRenderer<ApiRenderContext>
{
var outputFile = OutputFile(page);
var outputFile = OutputFile(renderContext.CurrentNavigation);
if (!outputFile.Directory!.Exists)
outputFile.Directory.Create();

await using var stream = _writeFileSystem.FileStream.New(outputFile.FullName, FileMode.OpenOrCreate);
await page.RenderAsync(stream, renderContext, ctx);
return outputFile;

IFileInfo OutputFile(IPageInformation pageInformation)
IFileInfo OutputFile(INavigationItem currentNavigation)
{
const string indexHtml = "index.html";
var fileName = pageInformation.Url + "/" + indexHtml;
var fileName = currentNavigation.Url + "/" + indexHtml;
var fileInfo = _writeFileSystem.FileInfo.New(Path.Combine(context.DocumentationOutputDirectory.FullName, fileName.Trim('/')));
return fileInfo;
}
Expand Down
53 changes: 18 additions & 35 deletions src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,52 @@
// See the LICENSE file in the project root for more information

using System.IO.Abstractions;
using Elastic.ApiExplorer.Endpoints;
using Elastic.ApiExplorer.Landing;
using Elastic.Documentation.Site.Navigation;
using Microsoft.OpenApi.Models;
using RazorSlices;

namespace Elastic.ApiExplorer.Operations;

public record ApiOperation : IPageInformation, IPageRenderer<ApiRenderContext>
public record ApiOperation(OperationType OperationType, OpenApiOperation Operation) : INavigationModel, IPageRenderer<ApiRenderContext>
{
public ApiOperation(string url, OperationType operationType, OpenApiOperation operation, IGroupNavigationItem navigationRoot)
{
OperationType = operationType;
Operation = operation;
NavigationRoot = navigationRoot;

//TODO
NavigationTitle = $"{operationType.ToString().ToLowerInvariant()} {operation.OperationId}";
CrossLink = "";
Url = url;
}

public string NavigationTitle { get; }
public string CrossLink { get; }
public string Url { get; }

public OperationType OperationType { get; }
public OpenApiOperation Operation { get; }
public IGroupNavigationItem NavigationRoot { get; }

public async Task RenderAsync(FileSystemStream stream, ApiRenderContext context, CancellationToken ctx = default)
public async Task RenderAsync(FileSystemStream stream, ApiRenderContext context, Cancel ctx = default)
{
var viewModel = new OperationViewModel
{
Operation = this,
StaticFileContentHashProvider = context.StaticFileContentHashProvider,
NavigationHtml = context.NavigationHtml
NavigationHtml = context.NavigationHtml,
CurrentNavigationItem = context.CurrentNavigation
};
var slice = OperationView.Create(viewModel);
await slice.RenderAsync(stream, cancellationToken: ctx);
}
}

public class OperationNavigationItem : IGroupNavigationItem
public class OperationNavigationItem : ILeafNavigationItem<ApiOperation>
{
public OperationNavigationItem(int depth, ApiOperation apiOperation, IGroupNavigationItem? parent, LandingNavigationItem root)
public OperationNavigationItem(int depth, string url, ApiOperation apiOperation, EndpointNavigationItem parent, LandingNavigationItem root)
{
Parent = parent;
Depth = depth;
//Current = group.Current;
NavigationRoot = root;
Id = NavigationRoot.Id;

Index = apiOperation;
Current = apiOperation;
Operation = apiOperation;
Model = apiOperation;
Url = url;
//TODO
NavigationTitle = $"{apiOperation.OperationType.ToString().ToLowerInvariant()} {apiOperation.Operation.OperationId}";
}

public IGroupNavigationItem NavigationRoot { get; }
public INodeNavigationItem<INavigationModel, INavigationItem> NavigationRoot { get; }
public string Id { get; }
public IGroupNavigationItem? Parent { get; set; }
public int Depth { get; }
public IPageInformation Current { get; }
public IPageInformation Index { get; }
public IReadOnlyCollection<INavigationItem> NavigationItems { get; set; } = [];
public ApiOperation Operation { get; set; }
public ApiOperation Model { get; }
public string Url { get; }

public string NavigationTitle { get; }

public INodeNavigationItem<INavigationModel, INavigationItem>? Parent { get; set; }
}
2 changes: 1 addition & 1 deletion src/Elastic.ApiExplorer/Operations/OperationView.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Description = "",
Layout = null,
PageTocItems = [],
CurrentDocument = Model.Operation,
CurrentNavigationItem = Model.CurrentNavigationItem,
Previous = null,
Next = null,
NavigationHtml = Model.NavigationHtml,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information

using System.Text.RegularExpressions;
using Elastic.Documentation.Serialization;
using YamlDotNet.Serialization;
using YamlStaticContext = Elastic.Documentation.Configuration.Serialization.YamlStaticContext;

Expand Down
3 changes: 1 addition & 2 deletions src/Elastic.Documentation.LinkIndex/LinkIndexReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// 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.Net;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
Expand All @@ -15,7 +14,7 @@ public class Aws3LinkIndexReader(IAmazonS3 s3Client, string bucketName = "elasti

// <summary>
// Using <see cref="AnonymousAWSCredentials"/> to access the link index
// allows to read from the link index without the need to provide AWS credentials.
// allows reading from the link index without the need to provide AWS credentials.
// </summary>
public static Aws3LinkIndexReader CreateAnonymous()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information

using System.Net;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Elastic.Documentation.Links;
Expand Down
Loading
Loading