Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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);

}
87 changes: 87 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,93 @@ public ApplicabilityRenderData RenderApplicability(
);
}

public ApplicabilityRenderData RenderCombinedApplicability(
IEnumerable<Applicability> applicabilities,
ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
VersioningSystem versioningSystem,
AppliesCollection allApplications)
{
var applicabilityList = applicabilities.ToList();
var primaryApplicability = GetPrimaryApplicability(applicabilityList, versioningSystem);

var primaryRenderData = RenderApplicability(primaryApplicability, applicabilityDefinition, versioningSystem, allApplications);
var combinedTooltip = BuildCombinedTooltipText(applicabilityList, applicabilityDefinition, versioningSystem);

return primaryRenderData with { TooltipText = combinedTooltip };
}

private static Applicability GetPrimaryApplicability(List<Applicability> applicabilities, VersioningSystem versioningSystem)
{
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 = applicabilities
.Where(a => a.Version is null || a.Version is AllVersions || a.Version <= versioningSystem.Current)
.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 = applicabilities
.Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > versioningSystem.Current)
.ToList();

if (futureApplicabilities.Count != 0)
{
return futureApplicabilities
.OrderBy(a => a.Version!.CompareTo(versioningSystem.Current))
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
.First();
}

return applicabilities.First();
}

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));

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);
tooltipParts.Add($"{heading}\n{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}" : "";
return $"{applicabilityDefinition.DisplayName} {lifecycleName}{versionText}:";
}

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
96 changes: 83 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,9 +120,86 @@ 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 = GetPrimaryApplicability(allApplicabilities, versioningSystem);

return new ApplicabilityItem(
Key: firstItem.Key,
PrimaryApplicability: primaryApplicability,
RenderData: combinedRenderData,
ApplicabilityDefinition: applicabilityDefinition
);
});


/// <summary>
/// Selects the most relevant applicability for display: available versions first (highest version), then closest future version
/// </summary>
private static Applicability GetPrimaryApplicability(List<Applicability> applicabilities, VersioningSystem versioningSystem)
{
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 = applicabilities
.Where(a => a.Version is null || a.Version is AllVersions || a.Version <= versioningSystem.Current)
.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 = applicabilities
.Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > versioningSystem.Current)
.ToList();

if (futureApplicabilities.Count != 0)
{
return futureApplicabilities
.OrderBy(a => a.Version!.CompareTo(versioningSystem.Current))
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
.First();
}

return applicabilities.First();
}

}
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
21 changes: 9 additions & 12 deletions tests/authoring/Inline/AppliesToRole.fs
Original file line number Diff line number Diff line change
Expand Up @@ -137,26 +137,23 @@ This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element.
))

[<Fact>]
let ``validate HTML: generates link and alt attr`` () =
let ``validate HTML: generates single combined badge`` () =
markdown |> convertsToHtml """
<p>This is an inline
<span class="applies applies-inline">
<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 9.1.0:
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.">
<span class="applicable-name">Stack</span>
<span class="applicable-separator"></span>
<span class="applicable-meta applicable-meta-ga">
GA 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.

Elastic&nbsp;Stack Preview 9.0.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 class="applicable-meta applicable-meta-ga">
GA planned
</span>
</span>
</span>
Expand Down
Loading