diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css
index a021fd476..312e4da4d 100644
--- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css
+++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css
@@ -12,6 +12,10 @@
@apply border-grey-20 inline-grid cursor-default grid-cols-[auto_1fr_auto] rounded-full border-[1px] bg-white pt-1.5 pr-3 pb-1.5 pl-3;
}
+ .applicable-meta {
+ @apply inline-flex gap-1.5;
+ }
+
.applicable-name,
.applicable-meta {
@apply text-xs text-nowrap;
@@ -45,4 +49,29 @@
font-size: 0.65em;
}
}
+
+ .applicable-ellipsis {
+ @apply bg-grey-20 inline-flex h-full items-center gap-0.5 rounded-md px-0.5 py-1;
+ }
+
+ .applicable-ellipsis__dot {
+ @apply bg-grey-50 size-1 rounded-full;
+ }
+}
+
+.tippy-box[data-theme~='applies-to'] {
+ .tippy-content {
+ white-space: normal;
+
+ strong {
+ display: block;
+ margin-bottom: calc(var(--spacing) * 1);
+ }
+ }
+
+ .tippy-content > div:not(:last-child) {
+ border-bottom: 1px dotted var(--color-grey-50);
+ padding-bottom: calc(var(--spacing) * 3);
+ margin-bottom: calc(var(--spacing) * 3);
+ }
}
diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts
index d222c8448..df5ad9d66 100644
--- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts
+++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts
@@ -1,14 +1,23 @@
+import { $$ } from 'select-dom'
import tippy from 'tippy.js'
document.addEventListener('htmx:load', function () {
- tippy(
- [
- '.applies [data-tippy-content]:not([data-tippy-content=""])',
- '.applies-inline [data-tippy-content]:not([data-tippy-content=""])',
- ].join(', '),
- {
+ const selector = [
+ '.applies [data-tippy-content]:not([data-tippy-content=""])',
+ '.applies-inline [data-tippy-content]:not([data-tippy-content=""])',
+ ].join(', ')
+
+ const appliesToBadgesWithTooltip = $$(selector)
+ appliesToBadgesWithTooltip.forEach((badge) => {
+ const content = badge.getAttribute('data-tippy-content')
+ if (!content) return
+ tippy(badge, {
+ content,
+ allowHTML: true,
delay: [400, 100],
hideOnClick: false,
- }
- )
+ ignoreAttributes: true,
+ theme: 'applies-to',
+ })
+ })
})
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
new file mode 100644
index 000000000..cb881fbf6
--- /dev/null
+++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
@@ -0,0 +1,58 @@
+// Licensed to Elasticsearch B.V under one or more agreements.
+// 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
+
+namespace Elastic.Documentation.AppliesTo;
+
+///
+/// Utility class for selecting the most relevant applicability from a collection of applicabilities.
+///
+public static class ApplicabilitySelector
+{
+ ///
+ /// Selects the most relevant applicability for display: available versions first (highest version), then closest future version
+ ///
+ /// The collection of applicabilities to select from
+ /// The current version to use for comparison
+ /// The most relevant applicability for display
+ public static Applicability GetPrimaryApplicability(IEnumerable applicabilities, SemVersion currentVersion)
+ {
+ var applicabilityList = applicabilities.ToList();
+ var lifecycleOrder = new Dictionary
+ {
+ [ProductLifecycle.GenerallyAvailable] = 0,
+ [ProductLifecycle.Beta] = 1,
+ [ProductLifecycle.TechnicalPreview] = 2,
+ [ProductLifecycle.Planned] = 3,
+ [ProductLifecycle.Deprecated] = 4,
+ [ProductLifecycle.Removed] = 5,
+ [ProductLifecycle.Unavailable] = 6
+ };
+
+ var availableApplicabilities = applicabilityList
+ .Where(a => a.Version is null || a.Version is AllVersions || a.Version <= currentVersion)
+ .ToList();
+
+ if (availableApplicabilities.Count != 0)
+ {
+ return availableApplicabilities
+ .OrderByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
+ .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
+ .First();
+ }
+
+ var futureApplicabilities = applicabilityList
+ .Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > currentVersion)
+ .ToList();
+
+ if (futureApplicabilities.Count != 0)
+ {
+ return futureApplicabilities
+ .OrderBy(a => a.Version!.CompareTo(currentVersion))
+ .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
+ .First();
+ }
+
+ return applicabilityList.First();
+ }
+}
diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityItem.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityItem.cs
index d5a71f78b..348009ad3 100644
--- a/src/Elastic.Markdown/Myst/Components/ApplicabilityItem.cs
+++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityItem.cs
@@ -8,6 +8,10 @@ namespace Elastic.Markdown.Myst.Components;
public record ApplicabilityItem(
string Key,
- Applicability Applicability,
- ApplicabilityRenderer.ApplicabilityRenderData RenderData
-);
+ Applicability PrimaryApplicability,
+ ApplicabilityRenderer.ApplicabilityRenderData RenderData,
+ ApplicabilityMappings.ApplicabilityDefinition ApplicabilityDefinition
+)
+{
+ public Applicability Applicability => PrimaryApplicability;
+}
diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityMappings.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityMappings.cs
index 138b04a0f..50aa3bbad 100644
--- a/src/Elastic.Markdown/Myst/Components/ApplicabilityMappings.cs
+++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityMappings.cs
@@ -54,4 +54,5 @@ public record ApplicabilityDefinition(string Key, string DisplayName, Versioning
// Generic product
public static readonly ApplicabilityDefinition Product = new("", "", VersioningSystemId.All);
+
}
diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs
index 87a683c89..8ffe34306 100644
--- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs
+++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs
@@ -17,7 +17,8 @@ public record ApplicabilityRenderData(
string TooltipText,
string LifecycleClass,
bool ShowLifecycleName,
- bool ShowVersion
+ bool ShowVersion,
+ bool HasMultipleLifecycles = false
);
public ApplicabilityRenderData RenderApplicability(
@@ -46,6 +47,66 @@ public ApplicabilityRenderData RenderApplicability(
);
}
+ public ApplicabilityRenderData RenderCombinedApplicability(
+ IEnumerable applicabilities,
+ ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
+ VersioningSystem versioningSystem,
+ AppliesCollection allApplications)
+ {
+ var applicabilityList = applicabilities.ToList();
+ var primaryApplicability = ApplicabilitySelector.GetPrimaryApplicability(applicabilityList, versioningSystem.Current);
+
+ var primaryRenderData = RenderApplicability(primaryApplicability, applicabilityDefinition, versioningSystem, allApplications);
+ var combinedTooltip = BuildCombinedTooltipText(applicabilityList, applicabilityDefinition, versioningSystem);
+
+ // Check if there are multiple different lifecycles
+ var hasMultipleLifecycles = applicabilityList.Select(a => a.Lifecycle).Distinct().Count() > 1;
+
+ return primaryRenderData with
+ {
+ TooltipText = combinedTooltip,
+ HasMultipleLifecycles = hasMultipleLifecycles,
+ ShowLifecycleName = primaryRenderData.ShowLifecycleName || (string.IsNullOrEmpty(primaryRenderData.BadgeLifecycleText) && hasMultipleLifecycles)
+ };
+ }
+
+
+ private static string BuildCombinedTooltipText(
+ List applicabilities,
+ ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
+ VersioningSystem versioningSystem)
+ {
+ var tooltipParts = new List();
+
+ // Order by the same logic as primary selection: available first (by version desc), then future (by version asc)
+ var orderedApplicabilities = applicabilities
+ .OrderByDescending(a => a.Version is null || a.Version is AllVersions || a.Version <= versioningSystem.Current ? 1 : 0)
+ .ThenByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
+ .ThenBy(a => a.Version ?? new SemVersion(0, 0, 0))
+ .ToList();
+
+ foreach (var applicability in orderedApplicabilities)
+ {
+ var realVersion = TryGetRealVersion(applicability, out var v) ? v : null;
+ var lifecycleFull = GetLifecycleFullText(applicability.Lifecycle);
+ var heading = CreateApplicabilityHeading(applicability, applicabilityDefinition, realVersion);
+ var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, realVersion, lifecycleFull);
+ // language=html
+ tooltipParts.Add($"{heading}{tooltipText}
");
+ }
+
+ return string.Join("\n\n", tooltipParts);
+ }
+
+ private static string CreateApplicabilityHeading(Applicability applicability, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
+ SemVersion? realVersion)
+ {
+ var lifecycleName = applicability.GetLifeCycleName();
+ var versionText = realVersion is not null ? $" {realVersion}" : "";
+ // language=html
+ return $"""{applicabilityDefinition.DisplayName} {lifecycleName}{versionText}:""";
+ }
+
private static string GetLifecycleFullText(ProductLifecycle lifecycle) => lifecycle switch
{
ProductLifecycle.GenerallyAvailable => "Available",
@@ -76,8 +137,10 @@ or ProductLifecycle.Beta
or ProductLifecycle.TechnicalPreview
or ProductLifecycle.Planned =>
$"We plan to add this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
- ProductLifecycle.Deprecated => $"We plan to deprecate this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
- ProductLifecycle.Removed => $"We plan to remove this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
+ ProductLifecycle.Deprecated =>
+ $"We plan to deprecate this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
+ ProductLifecycle.Removed =>
+ $"We plan to remove this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
_ => tooltipText
}
: $"{lifecycleFull} on {applicabilityDefinition.DisplayName} unless otherwise specified.";
@@ -91,8 +154,10 @@ or ProductLifecycle.TechnicalPreview
private static string? GetDisclaimer(ProductLifecycle lifecycle, VersioningSystemId versioningSystemId) => lifecycle switch
{
- ProductLifecycle.Beta => "Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.",
- ProductLifecycle.TechnicalPreview => "This functionality may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.",
+ ProductLifecycle.Beta =>
+ "Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.",
+ ProductLifecycle.TechnicalPreview =>
+ "This functionality may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.",
ProductLifecycle.GenerallyAvailable => versioningSystemId is VersioningSystemId.Stack
? "If this functionality is unavailable or behaves differently when deployed on ECH, ECE, ECK, or a self-managed installation, it will be indicated on the page."
: null,
diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml
index f35d221e9..a4f7aa3d6 100644
--- a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml
+++ b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml
@@ -12,7 +12,7 @@
@if (item.RenderData.ShowLifecycleName)
{
- @item.Applicability.GetLifeCycleName()
+ @item.PrimaryApplicability.GetLifeCycleName()
}
@if (item.RenderData.ShowVersion)
{
@@ -24,6 +24,14 @@
{
@item.RenderData.BadgeLifecycleText
}
+ @if (item.RenderData.HasMultipleLifecycles)
+ {
+
+
+
+
+
+ }
}
diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
index 773bf03aa..11e50c37a 100644
--- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
+++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
@@ -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 Elastic.Documentation;
using Elastic.Documentation.AppliesTo;
using Elastic.Documentation.Configuration.Versions;
@@ -15,7 +16,6 @@ public class ApplicableToViewModel
public required ApplicableTo AppliesTo { get; init; }
public required VersionsConfiguration VersionsConfig { get; init; }
- // Dictionary mapping property selectors to their applicability definitions
private static readonly Dictionary, ApplicabilityMappings.ApplicabilityDefinition> DeploymentMappings = new()
{
[d => d.Ess] = ApplicabilityMappings.Ech,
@@ -56,15 +56,14 @@ public class ApplicableToViewModel
[p => p.ApmAgentRum] = ApplicabilityMappings.ApmAgentRum
};
+
public IEnumerable GetApplicabilityItems()
{
var items = new List();
- // Process Stack
if (AppliesTo.Stack is not null)
items.AddRange(ProcessSingleCollection(AppliesTo.Stack, ApplicabilityMappings.Stack));
- // Process Serverless
if (AppliesTo.Serverless is not null)
{
items.AddRange(AppliesTo.Serverless.AllProjects is not null
@@ -72,24 +71,18 @@ public IEnumerable GetApplicabilityItems()
: ProcessMappedCollections(AppliesTo.Serverless, ServerlessMappings));
}
- // Process Deployment
if (AppliesTo.Deployment is not null)
items.AddRange(ProcessMappedCollections(AppliesTo.Deployment, DeploymentMappings));
- // Process Product Applicability
if (AppliesTo.ProductApplicability is not null)
items.AddRange(ProcessMappedCollections(AppliesTo.ProductApplicability, ProductMappings));
- // Process Generic Product
if (AppliesTo.Product is not null)
items.AddRange(ProcessSingleCollection(AppliesTo.Product, ApplicabilityMappings.Product));
- return items;
+ return CombineItemsByKey(items);
}
- ///
- /// Processes a single collection with its corresponding applicability definition
- ///
private IEnumerable ProcessSingleCollection(AppliesCollection collection, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition)
{
var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId);
@@ -97,7 +90,7 @@ private IEnumerable ProcessSingleCollection(AppliesCollection
}
///
- /// Processes multiple collections using a mapping dictionary to eliminate repetitive code
+ /// Uses mapping dictionary to eliminate repetitive code when processing multiple collections
///
private IEnumerable ProcessMappedCollections(T source, Dictionary, ApplicabilityMappings.ApplicabilityDefinition> mappings)
{
@@ -127,10 +120,45 @@ private IEnumerable ProcessApplicabilityCollection(
return new ApplicabilityItem(
Key: applicabilityDefinition.Key,
- Applicability: applicability,
- RenderData: renderData
+ PrimaryApplicability: applicability,
+ RenderData: renderData,
+ ApplicabilityDefinition: applicabilityDefinition
);
});
+ ///
+ /// Combines multiple applicability items with the same key into a single item with combined tooltip
+ ///
+ private IEnumerable CombineItemsByKey(List items) => items
+ .GroupBy(item => item.Key)
+ .Select(group =>
+ {
+ if (group.Count() == 1)
+ return group.First();
+
+ var firstItem = group.First();
+ var allApplicabilities = group.Select(g => g.Applicability).ToList();
+ var applicabilityDefinition = firstItem.ApplicabilityDefinition;
+ var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId);
+
+ var combinedRenderData = _applicabilityRenderer.RenderCombinedApplicability(
+ allApplicabilities,
+ applicabilityDefinition,
+ versioningSystem,
+ new AppliesCollection(allApplicabilities.ToArray()));
+
+ // Select the closest version to current as the primary display
+ var primaryApplicability = ApplicabilitySelector.GetPrimaryApplicability(allApplicabilities, versioningSystem.Current);
+
+ return new ApplicabilityItem(
+ Key: firstItem.Key,
+ PrimaryApplicability: primaryApplicability,
+ RenderData: combinedRenderData,
+ ApplicabilityDefinition: applicabilityDefinition
+ );
+ });
+
+
+
}
diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs
index fc6c07223..515e632d2 100644
--- a/tests/authoring/Applicability/ApplicableToComponent.fs
+++ b/tests/authoring/Applicability/ApplicableToComponent.fs
@@ -396,7 +396,7 @@ This functionality may be changed or removed in a future release. Elastic will w
type ``mixed lifecycles with ga planned`` () =
static let markdown = Setup.Markdown """
```{applies_to}
-stack: ga 8.8.0, preview 9.0.0
+stack: ga 8.8.0, preview 8.1.0
```
"""
@@ -404,22 +404,22 @@ stack: ga 8.8.0, preview 9.0.0
let ``renders GA planned when preview exists alongside GA`` () =
markdown |> convertsToHtml """
-
- Stack
-
-
- Planned
-
-
-
+Elastic Stack Preview 8.1.0:We plan to add this functionality in a future Elastic Stack update. Subject to change.
+
+This functionality may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.
">
Stack
GA planned
+
+
+
+
+
@@ -799,3 +799,39 @@ If this functionality is unavailable or behaves differently when deployed on ECH
"""
+
+// Test multiple lifecycles for same applicability key
+type ``multiple lifecycles same key`` () =
+ static let markdown = Setup.Markdown """
+```{applies_to}
+stack: ga 8.0.0, beta 8.1.0
+```
+"""
+
+ []
+ let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () =
+ markdown |> convertsToHtml """
+
+
+ Stack
+
+
+ GA
+
+ 8.0.0
+
+
+
+
+
+
+
+
+
+"""
diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs
index 9ecfb8193..2450d5af7 100644
--- a/tests/authoring/Inline/AppliesToRole.fs
+++ b/tests/authoring/Inline/AppliesToRole.fs
@@ -137,26 +137,23 @@ This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element.
))
[]
- let ``validate HTML: generates link and alt attr`` () =
+ let ``validate HTML: generates single combined badge`` () =
markdown |> convertsToHtml """
This is an inline
-
- Stack
-
-
- GA planned
-
-
-
Stack
-
- Planned
+
+ GA planned