Skip to content

Commit d05760c

Browse files
reakaleekMpdreamz
andauthored
Combine applies_to badges with the same key (#1900)
* Combine applies_to badges with the same key * Simplify * Extract duplicated GetPrimaryApplicability method to ApplicabilitySelector utility class - Created new ApplicabilitySelector class in Elastic.Documentation.AppliesTo namespace - Removed duplicated GetPrimaryApplicability method from ApplicabilityRenderer and ApplicableToViewModel - Updated both classes to use the new utility class - Improves code reusability and reduces duplication * Fix ApplicabilitySelector location and add missing using statements - Moved ApplicabilitySelector from Elastic.Documentation to Elastic.Markdown.Myst.Components - Added missing using statements for AllVersions and SemVersion - All tests now pass successfully * Refactor ApplicabilitySelector to use SemVersion parameter and move to domain layer - Changed parameter from VersioningSystem to SemVersion currentVersion - Moved ApplicabilitySelector back to Elastic.Documentation.AppliesTo namespace - Updated callers to pass versioningSystem.Current - Simplified interface and improved domain separation - All tests pass successfully * Style tooltip Bold heading and separator * Add ellipsis if an applies_to key has multiple lifecycles * Show GA lifecycle multi lifecycle state * Update src/Elastic.Documentation.Site/Assets/markdown/applies-to.css Co-authored-by: Martijn Laarman <[email protected]> * Update src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts Co-authored-by: Martijn Laarman <[email protected]> * Run prettier --------- Co-authored-by: Martijn Laarman <[email protected]>
1 parent 6f75191 commit d05760c

File tree

10 files changed

+288
-53
lines changed

10 files changed

+288
-53
lines changed

src/Elastic.Documentation.Site/Assets/markdown/applies-to.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
@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;
1313
}
1414

15+
.applicable-meta {
16+
@apply inline-flex gap-1.5;
17+
}
18+
1519
.applicable-name,
1620
.applicable-meta {
1721
@apply text-xs text-nowrap;
@@ -45,4 +49,29 @@
4549
font-size: 0.65em;
4650
}
4751
}
52+
53+
.applicable-ellipsis {
54+
@apply bg-grey-20 inline-flex h-full items-center gap-0.5 rounded-md px-0.5 py-1;
55+
}
56+
57+
.applicable-ellipsis__dot {
58+
@apply bg-grey-50 size-1 rounded-full;
59+
}
60+
}
61+
62+
.tippy-box[data-theme~='applies-to'] {
63+
.tippy-content {
64+
white-space: normal;
65+
66+
strong {
67+
display: block;
68+
margin-bottom: calc(var(--spacing) * 1);
69+
}
70+
}
71+
72+
.tippy-content > div:not(:last-child) {
73+
border-bottom: 1px dotted var(--color-grey-50);
74+
padding-bottom: calc(var(--spacing) * 3);
75+
margin-bottom: calc(var(--spacing) * 3);
76+
}
4877
}
Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1+
import { $$ } from 'select-dom'
12
import tippy from 'tippy.js'
23

34
document.addEventListener('htmx:load', function () {
4-
tippy(
5-
[
6-
'.applies [data-tippy-content]:not([data-tippy-content=""])',
7-
'.applies-inline [data-tippy-content]:not([data-tippy-content=""])',
8-
].join(', '),
9-
{
5+
const selector = [
6+
'.applies [data-tippy-content]:not([data-tippy-content=""])',
7+
'.applies-inline [data-tippy-content]:not([data-tippy-content=""])',
8+
].join(', ')
9+
10+
const appliesToBadgesWithTooltip = $$(selector)
11+
appliesToBadgesWithTooltip.forEach((badge) => {
12+
const content = badge.getAttribute('data-tippy-content')
13+
if (!content) return
14+
tippy(badge, {
15+
content,
16+
allowHTML: true,
1017
delay: [400, 100],
1118
hideOnClick: false,
12-
}
13-
)
19+
ignoreAttributes: true,
20+
theme: 'applies-to',
21+
})
22+
})
1423
})
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
namespace Elastic.Documentation.AppliesTo;
6+
7+
/// <summary>
8+
/// Utility class for selecting the most relevant applicability from a collection of applicabilities.
9+
/// </summary>
10+
public static class ApplicabilitySelector
11+
{
12+
/// <summary>
13+
/// Selects the most relevant applicability for display: available versions first (highest version), then closest future version
14+
/// </summary>
15+
/// <param name="applicabilities">The collection of applicabilities to select from</param>
16+
/// <param name="currentVersion">The current version to use for comparison</param>
17+
/// <returns>The most relevant applicability for display</returns>
18+
public static Applicability GetPrimaryApplicability(IEnumerable<Applicability> applicabilities, SemVersion currentVersion)
19+
{
20+
var applicabilityList = applicabilities.ToList();
21+
var lifecycleOrder = new Dictionary<ProductLifecycle, int>
22+
{
23+
[ProductLifecycle.GenerallyAvailable] = 0,
24+
[ProductLifecycle.Beta] = 1,
25+
[ProductLifecycle.TechnicalPreview] = 2,
26+
[ProductLifecycle.Planned] = 3,
27+
[ProductLifecycle.Deprecated] = 4,
28+
[ProductLifecycle.Removed] = 5,
29+
[ProductLifecycle.Unavailable] = 6
30+
};
31+
32+
var availableApplicabilities = applicabilityList
33+
.Where(a => a.Version is null || a.Version is AllVersions || a.Version <= currentVersion)
34+
.ToList();
35+
36+
if (availableApplicabilities.Count != 0)
37+
{
38+
return availableApplicabilities
39+
.OrderByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
40+
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
41+
.First();
42+
}
43+
44+
var futureApplicabilities = applicabilityList
45+
.Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > currentVersion)
46+
.ToList();
47+
48+
if (futureApplicabilities.Count != 0)
49+
{
50+
return futureApplicabilities
51+
.OrderBy(a => a.Version!.CompareTo(currentVersion))
52+
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
53+
.First();
54+
}
55+
56+
return applicabilityList.First();
57+
}
58+
}

src/Elastic.Markdown/Myst/Components/ApplicabilityItem.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ namespace Elastic.Markdown.Myst.Components;
88

99
public record ApplicabilityItem(
1010
string Key,
11-
Applicability Applicability,
12-
ApplicabilityRenderer.ApplicabilityRenderData RenderData
13-
);
11+
Applicability PrimaryApplicability,
12+
ApplicabilityRenderer.ApplicabilityRenderData RenderData,
13+
ApplicabilityMappings.ApplicabilityDefinition ApplicabilityDefinition
14+
)
15+
{
16+
public Applicability Applicability => PrimaryApplicability;
17+
}

src/Elastic.Markdown/Myst/Components/ApplicabilityMappings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ public record ApplicabilityDefinition(string Key, string DisplayName, Versioning
5454

5555
// Generic product
5656
public static readonly ApplicabilityDefinition Product = new("", "", VersioningSystemId.All);
57+
5758
}

src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public record ApplicabilityRenderData(
1717
string TooltipText,
1818
string LifecycleClass,
1919
bool ShowLifecycleName,
20-
bool ShowVersion
20+
bool ShowVersion,
21+
bool HasMultipleLifecycles = false
2122
);
2223

2324
public ApplicabilityRenderData RenderApplicability(
@@ -46,6 +47,66 @@ public ApplicabilityRenderData RenderApplicability(
4647
);
4748
}
4849

50+
public ApplicabilityRenderData RenderCombinedApplicability(
51+
IEnumerable<Applicability> applicabilities,
52+
ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
53+
VersioningSystem versioningSystem,
54+
AppliesCollection allApplications)
55+
{
56+
var applicabilityList = applicabilities.ToList();
57+
var primaryApplicability = ApplicabilitySelector.GetPrimaryApplicability(applicabilityList, versioningSystem.Current);
58+
59+
var primaryRenderData = RenderApplicability(primaryApplicability, applicabilityDefinition, versioningSystem, allApplications);
60+
var combinedTooltip = BuildCombinedTooltipText(applicabilityList, applicabilityDefinition, versioningSystem);
61+
62+
// Check if there are multiple different lifecycles
63+
var hasMultipleLifecycles = applicabilityList.Select(a => a.Lifecycle).Distinct().Count() > 1;
64+
65+
return primaryRenderData with
66+
{
67+
TooltipText = combinedTooltip,
68+
HasMultipleLifecycles = hasMultipleLifecycles,
69+
ShowLifecycleName = primaryRenderData.ShowLifecycleName || (string.IsNullOrEmpty(primaryRenderData.BadgeLifecycleText) && hasMultipleLifecycles)
70+
};
71+
}
72+
73+
74+
private static string BuildCombinedTooltipText(
75+
List<Applicability> applicabilities,
76+
ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
77+
VersioningSystem versioningSystem)
78+
{
79+
var tooltipParts = new List<string>();
80+
81+
// Order by the same logic as primary selection: available first (by version desc), then future (by version asc)
82+
var orderedApplicabilities = applicabilities
83+
.OrderByDescending(a => a.Version is null || a.Version is AllVersions || a.Version <= versioningSystem.Current ? 1 : 0)
84+
.ThenByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
85+
.ThenBy(a => a.Version ?? new SemVersion(0, 0, 0))
86+
.ToList();
87+
88+
foreach (var applicability in orderedApplicabilities)
89+
{
90+
var realVersion = TryGetRealVersion(applicability, out var v) ? v : null;
91+
var lifecycleFull = GetLifecycleFullText(applicability.Lifecycle);
92+
var heading = CreateApplicabilityHeading(applicability, applicabilityDefinition, realVersion);
93+
var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, realVersion, lifecycleFull);
94+
// language=html
95+
tooltipParts.Add($"<div>{heading}{tooltipText}</div>");
96+
}
97+
98+
return string.Join("\n\n", tooltipParts);
99+
}
100+
101+
private static string CreateApplicabilityHeading(Applicability applicability, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
102+
SemVersion? realVersion)
103+
{
104+
var lifecycleName = applicability.GetLifeCycleName();
105+
var versionText = realVersion is not null ? $" {realVersion}" : "";
106+
// language=html
107+
return $"""<strong>{applicabilityDefinition.DisplayName} {lifecycleName}{versionText}:</strong>""";
108+
}
109+
49110
private static string GetLifecycleFullText(ProductLifecycle lifecycle) => lifecycle switch
50111
{
51112
ProductLifecycle.GenerallyAvailable => "Available",
@@ -76,8 +137,10 @@ or ProductLifecycle.Beta
76137
or ProductLifecycle.TechnicalPreview
77138
or ProductLifecycle.Planned =>
78139
$"We plan to add this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
79-
ProductLifecycle.Deprecated => $"We plan to deprecate this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
80-
ProductLifecycle.Removed => $"We plan to remove this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
140+
ProductLifecycle.Deprecated =>
141+
$"We plan to deprecate this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
142+
ProductLifecycle.Removed =>
143+
$"We plan to remove this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.",
81144
_ => tooltipText
82145
}
83146
: $"{lifecycleFull} on {applicabilityDefinition.DisplayName} unless otherwise specified.";
@@ -91,8 +154,10 @@ or ProductLifecycle.TechnicalPreview
91154

92155
private static string? GetDisclaimer(ProductLifecycle lifecycle, VersioningSystemId versioningSystemId) => lifecycle switch
93156
{
94-
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.",
95-
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.",
157+
ProductLifecycle.Beta =>
158+
"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.",
159+
ProductLifecycle.TechnicalPreview =>
160+
"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.",
96161
ProductLifecycle.GenerallyAvailable => versioningSystemId is VersioningSystemId.Stack
97162
? "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."
98163
: null,

src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<span class="applicable-meta [email protected]">
1313
@if (item.RenderData.ShowLifecycleName)
1414
{
15-
<span class="applicable-lifecycle [email protected]">@item.Applicability.GetLifeCycleName()</span>
15+
<span class="applicable-lifecycle [email protected]">@item.PrimaryApplicability.GetLifeCycleName()</span>
1616
}
1717
@if (item.RenderData.ShowVersion)
1818
{
@@ -24,6 +24,14 @@
2424
{
2525
@item.RenderData.BadgeLifecycleText
2626
}
27+
@if (item.RenderData.HasMultipleLifecycles)
28+
{
29+
<span class="applicable-ellipsis">
30+
<span class="applicable-ellipsis__dot"></span>
31+
<span class="applicable-ellipsis__dot"></span>
32+
<span class="applicable-ellipsis__dot"></span>
33+
</span>
34+
}
2735
</span>
2836
</span>
2937
}

src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using Elastic.Documentation;
56
using Elastic.Documentation.AppliesTo;
67
using Elastic.Documentation.Configuration.Versions;
78

@@ -15,7 +16,6 @@ public class ApplicableToViewModel
1516
public required ApplicableTo AppliesTo { get; init; }
1617
public required VersionsConfiguration VersionsConfig { get; init; }
1718

18-
// Dictionary mapping property selectors to their applicability definitions
1919
private static readonly Dictionary<Func<DeploymentApplicability, AppliesCollection?>, ApplicabilityMappings.ApplicabilityDefinition> DeploymentMappings = new()
2020
{
2121
[d => d.Ess] = ApplicabilityMappings.Ech,
@@ -56,48 +56,41 @@ public class ApplicableToViewModel
5656
[p => p.ApmAgentRum] = ApplicabilityMappings.ApmAgentRum
5757
};
5858

59+
5960
public IEnumerable<ApplicabilityItem> GetApplicabilityItems()
6061
{
6162
var items = new List<ApplicabilityItem>();
6263

63-
// Process Stack
6464
if (AppliesTo.Stack is not null)
6565
items.AddRange(ProcessSingleCollection(AppliesTo.Stack, ApplicabilityMappings.Stack));
6666

67-
// Process Serverless
6867
if (AppliesTo.Serverless is not null)
6968
{
7069
items.AddRange(AppliesTo.Serverless.AllProjects is not null
7170
? ProcessSingleCollection(AppliesTo.Serverless.AllProjects, ApplicabilityMappings.Serverless)
7271
: ProcessMappedCollections(AppliesTo.Serverless, ServerlessMappings));
7372
}
7473

75-
// Process Deployment
7674
if (AppliesTo.Deployment is not null)
7775
items.AddRange(ProcessMappedCollections(AppliesTo.Deployment, DeploymentMappings));
7876

79-
// Process Product Applicability
8077
if (AppliesTo.ProductApplicability is not null)
8178
items.AddRange(ProcessMappedCollections(AppliesTo.ProductApplicability, ProductMappings));
8279

83-
// Process Generic Product
8480
if (AppliesTo.Product is not null)
8581
items.AddRange(ProcessSingleCollection(AppliesTo.Product, ApplicabilityMappings.Product));
8682

87-
return items;
83+
return CombineItemsByKey(items);
8884
}
8985

90-
/// <summary>
91-
/// Processes a single collection with its corresponding applicability definition
92-
/// </summary>
9386
private IEnumerable<ApplicabilityItem> ProcessSingleCollection(AppliesCollection collection, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition)
9487
{
9588
var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId);
9689
return ProcessApplicabilityCollection(collection, applicabilityDefinition, versioningSystem);
9790
}
9891

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

128121
return new ApplicabilityItem(
129122
Key: applicabilityDefinition.Key,
130-
Applicability: applicability,
131-
RenderData: renderData
123+
PrimaryApplicability: applicability,
124+
RenderData: renderData,
125+
ApplicabilityDefinition: applicabilityDefinition
132126
);
133127
});
134128

129+
/// <summary>
130+
/// Combines multiple applicability items with the same key into a single item with combined tooltip
131+
/// </summary>
132+
private IEnumerable<ApplicabilityItem> CombineItemsByKey(List<ApplicabilityItem> items) => items
133+
.GroupBy(item => item.Key)
134+
.Select(group =>
135+
{
136+
if (group.Count() == 1)
137+
return group.First();
138+
139+
var firstItem = group.First();
140+
var allApplicabilities = group.Select(g => g.Applicability).ToList();
141+
var applicabilityDefinition = firstItem.ApplicabilityDefinition;
142+
var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId);
143+
144+
var combinedRenderData = _applicabilityRenderer.RenderCombinedApplicability(
145+
allApplicabilities,
146+
applicabilityDefinition,
147+
versioningSystem,
148+
new AppliesCollection(allApplicabilities.ToArray()));
149+
150+
// Select the closest version to current as the primary display
151+
var primaryApplicability = ApplicabilitySelector.GetPrimaryApplicability(allApplicabilities, versioningSystem.Current);
152+
153+
return new ApplicabilityItem(
154+
Key: firstItem.Key,
155+
PrimaryApplicability: primaryApplicability,
156+
RenderData: combinedRenderData,
157+
ApplicabilityDefinition: applicabilityDefinition
158+
);
159+
});
160+
161+
162+
135163
}
136164

0 commit comments

Comments
 (0)