Skip to content
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
29 changes: 29 additions & 0 deletions src/Elastic.Documentation.Site/Assets/markdown/applies-to.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
25 changes: 17 additions & 8 deletions src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts
Original file line number Diff line number Diff line change
@@ -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',
})
})
})
58 changes: 58 additions & 0 deletions src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Utility class for selecting the most relevant applicability from a collection of applicabilities.
/// </summary>
public static class ApplicabilitySelector
{
/// <summary>
/// Selects the most relevant applicability for display: available versions first (highest version), then closest future version
/// </summary>
/// <param name="applicabilities">The collection of applicabilities to select from</param>
/// <param name="currentVersion">The current version to use for comparison</param>
/// <returns>The most relevant applicability for display</returns>
public static Applicability GetPrimaryApplicability(IEnumerable<Applicability> applicabilities, SemVersion currentVersion)
{
var applicabilityList = applicabilities.ToList();
var lifecycleOrder = new Dictionary<ProductLifecycle, int>
{
[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();
}
}
10 changes: 7 additions & 3 deletions src/Elastic.Markdown/Myst/Components/ApplicabilityItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ public record ApplicabilityDefinition(string Key, string DisplayName, Versioning

// Generic product
public static readonly ApplicabilityDefinition Product = new("", "", VersioningSystemId.All);

}
75 changes: 70 additions & 5 deletions src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public record ApplicabilityRenderData(
string TooltipText,
string LifecycleClass,
bool ShowLifecycleName,
bool ShowVersion
bool ShowVersion,
bool HasMultipleLifecycles = false
);

public ApplicabilityRenderData RenderApplicability(
Expand Down Expand Up @@ -46,6 +47,66 @@ public ApplicabilityRenderData RenderApplicability(
);
}

public ApplicabilityRenderData RenderCombinedApplicability(
IEnumerable<Applicability> 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<Applicability> applicabilities,
ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
VersioningSystem versioningSystem)
{
var tooltipParts = new List<string>();

// 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($"<div>{heading}{tooltipText}</div>");
}

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 $"""<strong>{applicabilityDefinition.DisplayName} {lifecycleName}{versionText}:</strong>""";
}

private static string GetLifecycleFullText(ProductLifecycle lifecycle) => lifecycle switch
{
ProductLifecycle.GenerallyAvailable => "Available",
Expand Down Expand Up @@ -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.";
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<span class="applicable-meta applicable-meta-@item.RenderData.LifecycleClass">
@if (item.RenderData.ShowLifecycleName)
{
<span class="applicable-lifecycle applicable-lifecycle-@item.RenderData.LifecycleClass">@item.Applicability.GetLifeCycleName()</span>
<span class="applicable-lifecycle applicable-lifecycle-@item.RenderData.LifecycleClass">@item.PrimaryApplicability.GetLifeCycleName()</span>
}
@if (item.RenderData.ShowVersion)
{
Expand All @@ -24,6 +24,14 @@
{
@item.RenderData.BadgeLifecycleText
}
@if (item.RenderData.HasMultipleLifecycles)
{
<span class="applicable-ellipsis">
<span class="applicable-ellipsis__dot"></span>
<span class="applicable-ellipsis__dot"></span>
<span class="applicable-ellipsis__dot"></span>
</span>
}
</span>
</span>
}
54 changes: 41 additions & 13 deletions src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.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 Elastic.Documentation;
using Elastic.Documentation.AppliesTo;
using Elastic.Documentation.Configuration.Versions;

Expand All @@ -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<Func<DeploymentApplicability, AppliesCollection?>, ApplicabilityMappings.ApplicabilityDefinition> DeploymentMappings = new()
{
[d => d.Ess] = ApplicabilityMappings.Ech,
Expand Down Expand Up @@ -56,48 +56,41 @@ public class ApplicableToViewModel
[p => p.ApmAgentRum] = ApplicabilityMappings.ApmAgentRum
};


public IEnumerable<ApplicabilityItem> GetApplicabilityItems()
{
var items = new List<ApplicabilityItem>();

// 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
? ProcessSingleCollection(AppliesTo.Serverless.AllProjects, ApplicabilityMappings.Serverless)
: 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);
}

/// <summary>
/// Processes a single collection with its corresponding applicability definition
/// </summary>
private IEnumerable<ApplicabilityItem> ProcessSingleCollection(AppliesCollection collection, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition)
{
var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId);
return ProcessApplicabilityCollection(collection, applicabilityDefinition, versioningSystem);
}

/// <summary>
/// Processes multiple collections using a mapping dictionary to eliminate repetitive code
/// Uses mapping dictionary to eliminate repetitive code when processing multiple collections
/// </summary>
private IEnumerable<ApplicabilityItem> ProcessMappedCollections<T>(T source, Dictionary<Func<T, AppliesCollection?>, ApplicabilityMappings.ApplicabilityDefinition> mappings)
{
Expand Down Expand Up @@ -127,10 +120,45 @@ private IEnumerable<ApplicabilityItem> ProcessApplicabilityCollection(

return new ApplicabilityItem(
Key: applicabilityDefinition.Key,
Applicability: applicability,
RenderData: renderData
PrimaryApplicability: applicability,
RenderData: renderData,
ApplicabilityDefinition: applicabilityDefinition
);
});

/// <summary>
/// Combines multiple applicability items with the same key into a single item with combined tooltip
/// </summary>
private IEnumerable<ApplicabilityItem> CombineItemsByKey(List<ApplicabilityItem> 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
);
});



}

Loading
Loading