Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
19 changes: 19 additions & 0 deletions src/Elastic.Documentation.Site/Assets/markdown/applies-to.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,22 @@
}
}
}

.tippy-box[data-theme~='applies-to'] {
/*background-color: tomato;*/
/*color: yellow;*/
.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);
}
}
45 changes: 35 additions & 10 deletions src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import tippy from 'tippy.js'
import { $$ } from 'select-dom'

document.addEventListener('htmx:load', function () {
tippy(
[
'.applies [data-tippy-content]:not([data-tippy-content=""])',
'.applies-inline [data-tippy-content]:not([data-tippy-content=""])',
].join(', '),
{
delay: [400, 100],
hideOnClick: false,
}
)

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'
}
)
})

// tippy(
// [
// '.applies [data-tippy-content]:not([data-tippy-content=""])',
// '.applies-inline [data-tippy-content]:not([data-tippy-content=""])',
// ].join(', '),
// {
// delay: [400, 100],
// hideOnClick: false,
// }
// )
})
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);

}
51 changes: 51 additions & 0 deletions src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,57 @@ 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);

return primaryRenderData with { TooltipText = combinedTooltip };
}


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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<span class="applicable-meta [email protected]">
@if (item.RenderData.ShowLifecycleName)
{
<span class="applicable-lifecycle [email protected]">@item.Applicability.GetLifeCycleName()</span>
<span class="applicable-lifecycle [email protected]">@item.PrimaryApplicability.GetLifeCycleName()</span>
}
@if (item.RenderData.ShowVersion)
{
Expand Down
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
);
});



}

19 changes: 8 additions & 11 deletions tests/authoring/Applicability/ApplicableToComponent.fs
Original file line number Diff line number Diff line change
Expand Up @@ -396,26 +396,23 @@ 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
```
"""

[<Fact>]
let ``renders GA planned when preview exists alongside GA`` () =
markdown |> convertsToHtml """
<p class="applies applies-block">
<span class="applicable-info" data-tippy-content="We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
<span class="applicable-info" data-tippy-content="Elastic&nbsp;Stack GA 8.8.0:
We plan to add this functionality in a future Elastic&nbsp;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.">
<span class="applicable-name">Stack</span>
<span class="applicable-separator"></span>
<span class="applicable-meta applicable-meta-preview">
Planned
</span>
</span>
<span class="applicable-info" data-tippy-content="We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
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.

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.">
Elastic&nbsp;Stack Preview 8.1.0:
We plan to add this functionality in a future Elastic&nbsp;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.">
<span class="applicable-name">Stack</span>
<span class="applicable-separator"></span>
<span class="applicable-meta applicable-meta-ga">
Expand Down
Loading
Loading