Skip to content

Commit 5853c59

Browse files
committed
Combine applies_to badges with the same key
1 parent 85a4887 commit 5853c59

File tree

7 files changed

+210
-40
lines changed

7 files changed

+210
-40
lines changed

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ 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+
IReadOnlyList<Applicability> AllApplicabilities,
13+
ApplicabilityRenderer.ApplicabilityRenderData RenderData,
14+
ApplicabilityMappings.ApplicabilityDefinition ApplicabilityDefinition
15+
)
16+
{
17+
public ApplicabilityItem(string key, Applicability applicability, ApplicabilityRenderer.ApplicabilityRenderData renderData)
18+
: this(key, applicability, [applicability], renderData, GetDefaultDefinition(key)) { }
19+
20+
public Applicability Applicability => PrimaryApplicability;
21+
22+
private static ApplicabilityMappings.ApplicabilityDefinition GetDefaultDefinition(string key) =>
23+
key switch
24+
{
25+
"Stack" => ApplicabilityMappings.Stack,
26+
"Serverless" => ApplicabilityMappings.Serverless,
27+
_ => ApplicabilityMappings.Product
28+
};
29+
}

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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,93 @@ public ApplicabilityRenderData RenderApplicability(
4646
);
4747
}
4848

49+
public ApplicabilityRenderData RenderCombinedApplicability(
50+
IEnumerable<Applicability> applicabilities,
51+
ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
52+
VersioningSystem versioningSystem,
53+
AppliesCollection allApplications)
54+
{
55+
var applicabilityList = applicabilities.ToList();
56+
var primaryApplicability = GetPrimaryApplicability(applicabilityList, versioningSystem);
57+
58+
var primaryRenderData = RenderApplicability(primaryApplicability, applicabilityDefinition, versioningSystem, allApplications);
59+
var combinedTooltip = BuildCombinedTooltipText(applicabilityList, applicabilityDefinition, versioningSystem);
60+
61+
return primaryRenderData with { TooltipText = combinedTooltip };
62+
}
63+
64+
private static Applicability GetPrimaryApplicability(List<Applicability> applicabilities, VersioningSystem versioningSystem)
65+
{
66+
var lifecycleOrder = new Dictionary<ProductLifecycle, int>
67+
{
68+
[ProductLifecycle.GenerallyAvailable] = 0,
69+
[ProductLifecycle.Beta] = 1,
70+
[ProductLifecycle.TechnicalPreview] = 2,
71+
[ProductLifecycle.Planned] = 3,
72+
[ProductLifecycle.Deprecated] = 4,
73+
[ProductLifecycle.Removed] = 5,
74+
[ProductLifecycle.Unavailable] = 6
75+
};
76+
77+
var availableApplicabilities = applicabilities
78+
.Where(a => a.Version is null || a.Version is AllVersions || a.Version <= versioningSystem.Current)
79+
.ToList();
80+
81+
if (availableApplicabilities.Count != 0)
82+
{
83+
return availableApplicabilities
84+
.OrderByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
85+
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
86+
.First();
87+
}
88+
89+
var futureApplicabilities = applicabilities
90+
.Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > versioningSystem.Current)
91+
.ToList();
92+
93+
if (futureApplicabilities.Count != 0)
94+
{
95+
return futureApplicabilities
96+
.OrderBy(a => a.Version!.CompareTo(versioningSystem.Current))
97+
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
98+
.First();
99+
}
100+
101+
return applicabilities.First();
102+
}
103+
104+
private static string BuildCombinedTooltipText(
105+
List<Applicability> applicabilities,
106+
ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition,
107+
VersioningSystem versioningSystem)
108+
{
109+
var tooltipParts = new List<string>();
110+
111+
// Order by the same logic as primary selection: available first (by version desc), then future (by version asc)
112+
var orderedApplicabilities = applicabilities
113+
.OrderByDescending(a => a.Version is null || a.Version is AllVersions || a.Version <= versioningSystem.Current ? 1 : 0)
114+
.ThenByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
115+
.ThenBy(a => a.Version ?? new SemVersion(0, 0, 0));
116+
117+
foreach (var applicability in orderedApplicabilities)
118+
{
119+
var realVersion = TryGetRealVersion(applicability, out var v) ? v : null;
120+
var lifecycleFull = GetLifecycleFullText(applicability.Lifecycle);
121+
var heading = CreateApplicabilityHeading(applicability, applicabilityDefinition, realVersion);
122+
var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, realVersion, lifecycleFull);
123+
tooltipParts.Add($"{heading}\n{tooltipText}");
124+
}
125+
126+
return string.Join("\n\n", tooltipParts);
127+
}
128+
129+
private static string CreateApplicabilityHeading(Applicability applicability, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, SemVersion? realVersion)
130+
{
131+
var lifecycleName = applicability.GetLifeCycleName();
132+
var versionText = realVersion is not null ? $" {realVersion}" : "";
133+
return $"{applicabilityDefinition.DisplayName} {lifecycleName}{versionText}:";
134+
}
135+
49136
private static string GetLifecycleFullText(ProductLifecycle lifecycle) => lifecycle switch
50137
{
51138
ProductLifecycle.GenerallyAvailable => "Available",

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

Lines changed: 1 addition & 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
{

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

Lines changed: 85 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,9 +120,88 @@ private IEnumerable<ApplicabilityItem> ProcessApplicabilityCollection(
127120

128121
return new ApplicabilityItem(
129122
Key: applicabilityDefinition.Key,
130-
Applicability: applicability,
131-
RenderData: renderData
123+
PrimaryApplicability: applicability,
124+
AllApplicabilities: [applicability],
125+
RenderData: renderData,
126+
ApplicabilityDefinition: applicabilityDefinition
132127
);
133128
});
134129

130+
/// <summary>
131+
/// Combines multiple applicability items with the same key into a single item with combined tooltip
132+
/// </summary>
133+
private IEnumerable<ApplicabilityItem> CombineItemsByKey(List<ApplicabilityItem> items) => items
134+
.GroupBy(item => item.Key)
135+
.Select(group =>
136+
{
137+
if (group.Count() == 1)
138+
return group.First();
139+
140+
var firstItem = group.First();
141+
var allApplicabilities = group.Select(g => g.Applicability).ToList();
142+
var applicabilityDefinition = firstItem.ApplicabilityDefinition;
143+
var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId);
144+
145+
var combinedRenderData = _applicabilityRenderer.RenderCombinedApplicability(
146+
allApplicabilities,
147+
applicabilityDefinition,
148+
versioningSystem,
149+
new AppliesCollection(allApplicabilities.ToArray()));
150+
151+
// Select the closest version to current as the primary display
152+
var primaryApplicability = GetPrimaryApplicability(allApplicabilities, versioningSystem);
153+
154+
return new ApplicabilityItem(
155+
Key: firstItem.Key,
156+
PrimaryApplicability: primaryApplicability,
157+
AllApplicabilities: allApplicabilities,
158+
RenderData: combinedRenderData,
159+
ApplicabilityDefinition: applicabilityDefinition
160+
);
161+
});
162+
163+
164+
/// <summary>
165+
/// Selects the most relevant applicability for display: available versions first (highest version), then closest future version
166+
/// </summary>
167+
private static Applicability GetPrimaryApplicability(List<Applicability> applicabilities, VersioningSystem versioningSystem)
168+
{
169+
var lifecycleOrder = new Dictionary<ProductLifecycle, int>
170+
{
171+
[ProductLifecycle.GenerallyAvailable] = 0,
172+
[ProductLifecycle.Beta] = 1,
173+
[ProductLifecycle.TechnicalPreview] = 2,
174+
[ProductLifecycle.Planned] = 3,
175+
[ProductLifecycle.Deprecated] = 4,
176+
[ProductLifecycle.Removed] = 5,
177+
[ProductLifecycle.Unavailable] = 6
178+
};
179+
180+
var availableApplicabilities = applicabilities
181+
.Where(a => a.Version is null || a.Version is AllVersions || a.Version <= versioningSystem.Current)
182+
.ToList();
183+
184+
if (availableApplicabilities.Count != 0)
185+
{
186+
return availableApplicabilities
187+
.OrderByDescending(a => a.Version ?? new SemVersion(0, 0, 0))
188+
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
189+
.First();
190+
}
191+
192+
var futureApplicabilities = applicabilities
193+
.Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > versioningSystem.Current)
194+
.ToList();
195+
196+
if (futureApplicabilities.Count != 0)
197+
{
198+
return futureApplicabilities
199+
.OrderBy(a => a.Version!.CompareTo(versioningSystem.Current))
200+
.ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999))
201+
.First();
202+
}
203+
204+
return applicabilities.First();
205+
}
206+
135207
}

tests/authoring/Applicability/ApplicableToComponent.fs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -396,26 +396,23 @@ This functionality may be changed or removed in a future release. Elastic will w
396396
type ``mixed lifecycles with ga planned`` () =
397397
static let markdown = Setup.Markdown """
398398
```{applies_to}
399-
stack: ga 8.8.0, preview 9.0.0
399+
stack: ga 8.8.0, preview 8.1.0
400400
```
401401
"""
402402

403403
[<Fact>]
404404
let ``renders GA planned when preview exists alongside GA`` () =
405405
markdown |> convertsToHtml """
406406
<p class="applies applies-block">
407-
<span class="applicable-info" data-tippy-content="We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
407+
<span class="applicable-info" data-tippy-content="Elastic&nbsp;Stack GA 8.8.0:
408+
We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
408409
409-
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.">
410-
<span class="applicable-name">Stack</span>
411-
<span class="applicable-separator"></span>
412-
<span class="applicable-meta applicable-meta-preview">
413-
Planned
414-
</span>
415-
</span>
416-
<span class="applicable-info" data-tippy-content="We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
410+
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.
417411
418-
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.">
412+
Elastic&nbsp;Stack Preview 8.1.0:
413+
We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
414+
415+
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.">
419416
<span class="applicable-name">Stack</span>
420417
<span class="applicable-separator"></span>
421418
<span class="applicable-meta applicable-meta-ga">

tests/authoring/Inline/AppliesToRole.fs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -137,26 +137,23 @@ This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element.
137137
))
138138

139139
[<Fact>]
140-
let ``validate HTML: generates link and alt attr`` () =
140+
let ``validate HTML: generates single combined badge`` () =
141141
markdown |> convertsToHtml """
142142
<p>This is an inline
143143
<span class="applies applies-inline">
144-
<span class="applicable-info" data-tippy-content="We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
144+
<span class="applicable-info" data-tippy-content="Elastic&nbsp;Stack GA 9.1.0:
145+
We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
145146
146-
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.">
147-
<span class="applicable-name">Stack</span>
148-
<span class="applicable-separator"></span>
149-
<span class="applicable-meta applicable-meta-ga">
150-
GA planned
151-
</span>
152-
</span>
153-
<span class="applicable-info" data-tippy-content="We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
147+
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.
148+
149+
Elastic&nbsp;Stack Preview 9.0.0:
150+
We plan to add this functionality in a future Elastic&nbsp;Stack update. Subject to change.
154151
155152
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.">
156153
<span class="applicable-name">Stack</span>
157154
<span class="applicable-separator"></span>
158-
<span class="applicable-meta applicable-meta-preview">
159-
Planned
155+
<span class="applicable-meta applicable-meta-ga">
156+
GA planned
160157
</span>
161158
</span>
162159
</span>

0 commit comments

Comments
 (0)