Skip to content
Open
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
4 changes: 3 additions & 1 deletion docs/data/openapi/api-explorer.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The API title comes from `info.title` in the spec, so fix naming and description

API search matches parameter and field names, including prefixes and qualified paths. The docs-wide search matches a field only by its exact name, and only for a word that contains `_` or `.`, so a plain word keeps ranking pages first. Direct operation and endpoint matches rank above field matches. An operation also matches the words of its operation id and path, split on dots, hyphens, underscores and case changes, and the name of its group, so `ml flush`, `flush ML job` and `_ml _flush` all find `POST /_ml/anomaly_detectors/{job_id}/_flush`. Combine an endpoint and a field, such as `_search knn`, to find the operation and its matching fields. The order does not matter: `knn /_search`, `/_search knn`, `knn search` and `search knn` read the same. A word with a leading slash, or an HTTP method word right before one, is always the endpoint; on its own a word such as `options` or `delete` may name a field. With two plain words the index tries both orders and keeps the one whose endpoint exists, so both halves must match. With three or more plain words the last word is the field. Each matching operation lists up to three matching fields beneath it, with their paths, types, and locations. Child rows match the field's own name; ancestor names and descriptions provide context without adding unrelated children. Select a field to open its anchor, including fields inside collapsed sections. Arrow keys move through operations and fields in order; Enter opens the selected row.

When a query names a path, such as `_search`, the result shows one route in place of the description: the method the page leads with and the path with the match highlighted. The route whose whole path is the query word wins, so `/_search` shows `/_search`; otherwise the operation's primary route shows when it contains the word, and an alternate route only when the primary does not. Every result uses the same bordered box, with matching fields grouped beneath the operation when present. Matches use the same yellow tint as the destination property's flash. Navigating to a field reveals it and flashes its name, including when search replaces the current page without a full reload. Each result's breadcrumb ends with the operation's group, the same group the page shows.
When a query names a path, such as `_search`, the result shows one route in place of the description: the method the page leads with and the path with the match highlighted. The route whose whole path is the query word wins, so `/_search` shows `/_search`; otherwise the operation's primary route shows when it contains the word, and an alternate route only when the primary does not. Every result uses the same bordered box, with matching fields grouped beneath the operation when present. Matches use the same yellow tint as the destination property's flash. Navigating to a field reveals it and flashes its name, including when search replaces the current page without a full reload. Each result's breadcrumb ends with the operation's group, the same group the page shows. A deprecated operation is struck through and dimmed in the results, its method chip turns grey as in the sidebar, and the same outlined `Deprecated` badge as on its page sits beside the title at the method chip's height.

The assembler indexes fields from the same property trees used to render operation pages. Fields of a repeated shape appear under its first listing. On the existing semantic index, the indexer adds the nested `api.properties` mapping before incremental synchronization. Property changes participate in the document hash, so a field update is synchronized even when the operation description stays the same.

Expand Down Expand Up @@ -466,6 +466,8 @@ Every endpoint with authentication has an `Auth` badge in the row under its path

A spec that declares no `security` shows no badge.

The sidebar marks an operation that the spec sets `deprecated: true` on, in both the legacy and the design-system look. Its title is struck through and dimmed, and its method chip turns grey. Screen readers hear the word "Deprecated" after the title. The row keeps its width, so long titles wrap as before.

Each listed scheme is resolved against `components.securitySchemes`, in the order the spec lists them:

| OpenAPI scheme | Label |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public IEnumerable<DocumentationDocument> ConvertToDocuments(OpenApiDocument ope
Method = method,
Path = path.Key,
OperationId = operationId,
Deprecated = operation.Value.Deprecated
Deprecated = apiOperation.Deprecated
};

yield return new DocumentationDocument
Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.ApiExplorer/Landing/ApiOverviewRow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ bool designSystem
showTooltip: false,
designSystem: designSystem
),
Deprecated = operation.Model.Operation.Deprecated,
Deprecated = operation.Model.Deprecated,
DesignSystem = designSystem
};
}
6 changes: 3 additions & 3 deletions src/Elastic.ApiExplorer/Operations/OperationCommonMark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static string Write(ApiOperation apiOperation, OperationPageModel page, A
var operation = apiOperation.Operation;
var title = OpenApiExtensionReader.GetSummary(operation) ?? apiOperation.DisplayName;
ApiCommonMark.Heading(markdown, 1, title);
WriteBadges(markdown, operation, page);
WriteBadges(markdown, apiOperation.Deprecated, page);
WriteServers(markdown, page);
WritePaths(markdown, page);
WritePathParameters(markdown, page, apiBaseUrl);
Expand All @@ -40,9 +40,9 @@ public static string Write(ApiOperation apiOperation, OperationPageModel page, A
return markdown.ToString();
}

private static void WriteBadges(StringBuilder markdown, OpenApiOperation operation, OperationPageModel page)
private static void WriteBadges(StringBuilder markdown, bool deprecated, OperationPageModel page)
{
if (operation.Deprecated)
if (deprecated)
ApiCommonMark.Paragraph(markdown, "deprecated");
if (page.AuthBadge is { } auth)
ApiCommonMark.Paragraph(markdown, $"Authentication: {auth.Summary}");
Expand Down
10 changes: 8 additions & 2 deletions src/Elastic.ApiExplorer/Operations/OperationNavigationItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ public record ApiOperation(
string Route,
IOpenApiPathItem Path,
string ApiName
) : IApiModel<OperationPageModel>, IHttpMethodNavigationModel
) : IApiModel<OperationPageModel>, IApiOperationNavigationModel
{
// The page header leads with the dominant method, which can come from the description's method listing
// rather than the spec; the sidebar badge must agree with it.
string IHttpMethodNavigationModel.HttpMethod => OperationEndpoint.Build(this, Operation.Description, []).Rows[0].Method;
string IApiOperationNavigationModel.HttpMethod => OperationEndpoint.Build(this, Operation.Description, []).Rows[0].Method;

/// <summary>
/// The one deprecation fact the page title badge, the overview rows, the sidebar row and the search index all read.
/// Collapsed siblings always agree on it, because operations that differ in lifecycle are never collapsed onto one page.
/// </summary>
public bool Deprecated => Operation.Deprecated;

/// <summary>The title shown for the operation; an operationId used as the grouping key is shown as readable words.</summary>
public string DisplayName =>
Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.ApiExplorer/Operations/OperationView.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
</li>
}
</ul>
@await RenderPartialAsync(_TitleBadges.Create(new TitleBadgesModel(pageModel.Availability, operation.Deprecated, Model.BuildContext.Configuration.Features.NavigationPreviewEnabled, pageModel.AuthBadge?.Text, Model.AuthBadgeHref)))
@await RenderPartialAsync(_TitleBadges.Create(new TitleBadgesModel(pageModel.Availability, Model.Operation.Deprecated, Model.BuildContext.Configuration.Features.NavigationPreviewEnabled, pageModel.AuthBadge?.Text, Model.AuthBadgeHref)))
</header>
<section id="elastic-api-v3">
@if (!string.IsNullOrWhiteSpace(pageModel.DescriptionMarkdown) || pageModel.ExternalDocs is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
namespace Elastic.Documentation.Navigation;

/// <summary>
/// Navigation model for an API operation leaf that exposes its HTTP method for sidebar chrome.
/// Navigation model for an API operation leaf that exposes its HTTP method and deprecation for sidebar chrome.
/// </summary>
public interface IHttpMethodNavigationModel : INavigationModel
public interface IApiOperationNavigationModel : INavigationModel
{
/// <summary>Lowercase HTTP method name (e.g. <c>get</c>, <c>post</c>).</summary>
string HttpMethod { get; }

/// <summary>True when the spec marks the operation deprecated.</summary>
bool Deprecated { get; }
}
19 changes: 19 additions & 0 deletions src/Elastic.Documentation.Site/Assets/api-docs.css
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,25 @@
border-color: var(--color-grey-100);
}

#pages-nav a.nav-link--deprecated > span:first-child {
text-decoration: line-through;
opacity: 0.7;
}

#pages-nav a.nav-link--deprecated .nav-v2-api-method {
background-color: var(--color-grey-20);
border-color: var(--color-grey-30);
color: var(--color-grey-100);
}

#pages-nav a.nav-link--deprecated:hover .nav-v2-api-method {
border-color: var(--color-grey-60);
}

#pages-nav a.nav-link--deprecated.current .nav-v2-api-method {
border-color: var(--color-grey-100);
}

.api-url {
margin-left: calc(var(--spacing) * 2);
min-width: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,25 @@ describe('property rows', () => {
expect(navigationSearchStore.getState().selectedIndex).toBe(3)
})
})

describe('deprecated operations', () => {
beforeEach(() => {
navigationSearchStore.getState().actions.setSearchTerm('search')
})

it('shows a Deprecated badge beside the title of a deprecated operation', () => {
const { container } = renderList({
groups: groupSearchResults([{ ...operation, deprecated: true }]),
})
const badges = container.querySelectorAll(
'.title-text .api-title-badges .applicable-info.api-badge--danger'
)
expect(badges).toHaveLength(1)
expect(badges[0]).toHaveTextContent('Deprecated')
})

it('shows no badge for an operation that is not deprecated', () => {
const { container } = renderList()
expect(container.querySelector('.api-badge--danger')).toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@ const SearchResultRow = ({
}
}
}

${
result.deprecated
? `.route-line .api-method { background-color: var(--color-grey-20); border-color: var(--color-grey-30); color: var(--color-grey-100); }`
: ''
}
`}
>
<div
Expand All @@ -322,7 +328,7 @@ const SearchResultRow = ({
`}
>
<Breadcrumb items={breadcrumbItems} />
<Title text={result.title} />
<Title text={result.title} deprecated={result.deprecated} />
{route ? (
<RouteLine route={route} tokens={tokens} />
) : (
Expand Down Expand Up @@ -415,7 +421,13 @@ const Breadcrumb = ({ items }: { items: string[] }) => {
)
}

const Title = ({ text }: { text: string }) => {
const Title = ({
text,
deprecated,
}: {
text: string
deprecated?: boolean
}) => {
const { euiTheme } = useEuiTheme()

return (
Expand All @@ -433,9 +445,54 @@ const Title = ({ text }: { text: string }) => {
font-weight: ${euiTheme.font.weight.bold};
color: ${euiTheme.colors.textParagraph};
}

${
deprecated
? `& > span:first-child { text-decoration: line-through; opacity: 0.7; }`
: ''
}
`}
>
<SanitizedHtmlContent htmlContent={text} />
{deprecated && (
<span
className="api-title-badges"
css={css`
/* An atomic inline, so the row's hover underline stops at the title text. Middle alignment lands 1px low beside the bold title. */
display: inline-flex;
align-items: center;
position: relative;
top: -1px;
margin-left: ${euiTheme.size.s};
vertical-align: middle;
line-height: 1;
font-weight: ${euiTheme.font.weight.regular};
.applies {
display: inline-flex;
margin-right: 0;
}
.applicable-info {
display: inline-flex;
align-items: center;
height: calc(
${euiTheme.size.base} + ${euiTheme.size.xxs}
);
padding: 0 ${euiTheme.size.s};
line-height: 1;
}
.applicable-name {
font-size: 10px;
line-height: 1;
}
`}
>
<span className="applies applies-inline">
<span className="applicable-info api-badge--danger">
<span className="applicable-name">Deprecated</span>
</span>
</span>
</span>
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('SearchResponse', () => {
title: 'Bulk API',
description: 'Index multiple documents',
score: 0.9,
deprecated: true,
parents: [
{ url: '/docs/api', title: 'API Reference' },
{
Expand All @@ -36,5 +37,9 @@ describe('SearchResponse', () => {
})

expect(parsed.results.map((item) => item.type)).toEqual(['docs', 'api'])
expect(parsed.results.map((item) => item.deprecated)).toEqual([
undefined,
true,
])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const SearchResultItem = z.object({
parents: z.array(SearchResultItemParent),
properties: z.array(ApiPropertyResult).optional(),
routes: z.array(ApiRoute).optional(),
deprecated: z.boolean().optional(),
})

export type SearchResultItem = z.infer<typeof SearchResultItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private static NavigationRenderNode Group(string id, string title, string url, p
"#",
Leaf("Search documents", "#", method: "post"),
Leaf("Get document", "#", method: "get"),
Leaf("Get document source", "#", method: "get") with { Deprecated = true },
Leaf("Delete document", "#", method: "delete")
),
Leaf("Query DSL", "#", badge: "ns"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
}
else
{
<a class="nav-tree__link" href="@item.Url"@(item.Url.Contains('#') ? new HtmlString(" hx-boost=\"false\"") : HtmlString.Empty)>
<a class="nav-tree__link@(item.Deprecated ? " nav-tree__link--deprecated" : "")" href="@item.Url"@(item.Url.Contains('#') ? new HtmlString(" hx-boost=\"false\"") : HtmlString.Empty)>
@await RenderPartialAsync(_NavTreeLabel.Create(item))
</a>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
@inherits RazorSlice<Elastic.Documentation.Site.Navigation.NavigationRenderNode>
@using Elastic.Documentation.Site.Components.Navigation.NavTree
<span class="nav-tree__text">@Model.NavigationTitle</span>
@if (Model.Deprecated)
{
<span class="sr-only">Deprecated</span>
}
@if (Model.Badge is { } badge)
{
@await RenderPartialAsync(_Badge.Create(new BadgeModel { Text = badge, Tone = Tone.Info }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@
color: var(--color-text-muted);
}

.nav-tree__link--deprecated .nav-tree__text {
text-decoration: line-through;
opacity: 0.7;
}

.nav-tree__link--deprecated .method {
--badge-tone: var(--color-text-muted);
--badge-surface: var(--color-surface-sunken);
}

.nav-tree__separator {
height: var(--border-width);
margin: var(--space-2) var(--space-3);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public sealed record NavigationRenderNode
public string? Badge { get; init; }
/// <summary>Lowercase HTTP method for API operation leaves; drives the sidebar method glyph.</summary>
public string? HttpMethod { get; init; }
/// <summary>True when the row groups several HTTP operations under one endpoint.</summary>
/// <summary>True for an API operation leaf the spec marks deprecated.</summary>
public bool Deprecated { get; init; }
/// <summary>Only projected for nodes, where it drives the expand/collapse checkbox and its persisted state.</summary>
public string? Id { get; init; }
public bool ShowToggle { get; init; }
Expand Down Expand Up @@ -396,16 +397,15 @@ private static NavigationRenderNode CreateNode(INodeNavigationItem<INavigationMo
private static NavigationRenderNode CreateLeaf(INavigationItem item, bool isTopLevel)
{
var (badge, navigationTitle) = ParseNavTitle(item.NavigationTitle);
var httpMethod = item is ILeafNavigationItem<INavigationModel> { Model: IHttpMethodNavigationModel method }
? method.HttpMethod
: null;
var operation = item is ILeafNavigationItem<INavigationModel> { Model: IApiOperationNavigationModel op } ? op : null;
return new NavigationRenderNode
{
Kind = NavigationRenderNodeKind.Leaf,
IsTopLevel = isTopLevel,
NavigationTitle = navigationTitle,
Badge = badge,
HttpMethod = httpMethod,
HttpMethod = operation?.HttpMethod,
Deprecated = operation?.Deprecated ?? false,
Url = item.Url
};
}
Expand Down Expand Up @@ -469,9 +469,11 @@ private static void AppendNode(IncrementalHash hash, NavigationRenderNode node)
Append(hash, node.Url);
Append(hash, node.Id ?? string.Empty);
AppendInt(hash, node.ShowToggle ? 1 : 0);
// Hashed only when set, so trees without an expanded group keep the hash, and so the tree id, they had before.
// Flags are hashed only when set, each with its own marker, so trees without them keep the hash, and so the tree id, they had before.
if (node.Expanded)
AppendInt(hash, 1);
if (node.Deprecated)
AppendInt(hash, 2);
AppendNodes(hash, node.NavigationItems);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
else if (item.Kind == NavigationRenderNodeKind.Leaf)
{
<li class="flex group/li">
<a href="@item.Url" class="sidebar-link nav-link nav-v2-link"@(item.Url.Contains('#') ? new Microsoft.AspNetCore.Html.HtmlString(" hx-boost=\"false\"") : Microsoft.AspNetCore.Html.HtmlString.Empty)>
<a href="@item.Url" class="sidebar-link nav-link nav-v2-link@(item.Deprecated ? " nav-link--deprecated" : "")"@(item.Url.Contains('#') ? new Microsoft.AspNetCore.Html.HtmlString(" hx-boost=\"false\"") : Microsoft.AspNetCore.Html.HtmlString.Empty)>
<span class="nav-v2-nav-text">@item.NavigationTitle</span>
@if (item.Deprecated) { <span class="sr-only">Deprecated</span> }
@if (item.Badge is not null) { <span class="nav-badge nav-badge-@item.Badge">@item.Badge</span> }
@await RenderPartialAsync(_NavHttpMethodGlyph.Create(item))
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
else if (item.Kind == NavigationRenderNodeKind.Leaf)
{
<li class="flex group/li pr-8 @(item.IsTopLevel ? "font-semibold mt-6" : "mt-4")">
<a href="@item.Url" class="sidebar-link nav-link"@(item.Url.Contains('#') ? new Microsoft.AspNetCore.Html.HtmlString(" hx-boost=\"false\"") : Microsoft.AspNetCore.Html.HtmlString.Empty)>
<a href="@item.Url" class="sidebar-link nav-link@(item.Deprecated ? " nav-link--deprecated" : "")"@(item.Url.Contains('#') ? new Microsoft.AspNetCore.Html.HtmlString(" hx-boost=\"false\"") : Microsoft.AspNetCore.Html.HtmlString.Empty)>
<span>@item.NavigationTitle</span>
@if (item.Deprecated) { <span class="sr-only">Deprecated</span> }
@if (item.Badge is not null) { <span class="nav-badge nav-badge-@item.Badge">@item.Badge</span> }
@await RenderPartialAsync(_NavHttpMethodGlyph.Create(item))
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ public partial class DefaultSearchService<TDocument>(
"api.method",
"api.path",
"api.operation_id",
"api.routes"
"api.routes",
// optional, so the required-member test cannot catch its absence; without it every hit reads as current
"api.deprecated"
];

private static readonly string[] SearchSourceIncludes =
Expand Down
Loading
Loading