Skip to content
Draft
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
176 changes: 176 additions & 0 deletions src/Elastic.Documentation/AppliesTo/ApplicableToOrderComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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>
/// Comparer for ordering ApplicableTo objects according to documentation standards.
/// Orders by:
/// 1. Serverless first, then Stack (latest to oldest)
/// 2. For deployments: ech/ess, ece, eck, self-managed
/// 3. Unavailable lifecycle comes last
/// </summary>
public class ApplicableToOrderComparer : IComparer<ApplicableTo?>
{
public int Compare(ApplicableTo? x, ApplicableTo? y)
{
if (ReferenceEquals(x, y))
return 0;
if (x is null)
return 1;
if (y is null)
return -1;

// Check if either has unavailable lifecycle - unavailable goes last
var xIsUnavailable = IsUnavailable(x);
var yIsUnavailable = IsUnavailable(y);

if (xIsUnavailable && !yIsUnavailable)
return 1;
if (!xIsUnavailable && yIsUnavailable)
return -1;

// Both unavailable or both available, continue with normal ordering

// Determine the primary category for each
var xCategory = GetPrimaryCategory(x);
var yCategory = GetPrimaryCategory(y);

// Compare by category first
var categoryComparison = xCategory.CompareTo(yCategory);
if (categoryComparison != 0)
return categoryComparison;

// Within the same category, apply specific ordering rules
return xCategory switch
{
ApplicabilityCategory.Serverless => 0,
ApplicabilityCategory.Stack => CompareStack(x, y),
ApplicabilityCategory.Deployment => CompareDeployment(x, y),
_ => 0
};
}

private static bool IsUnavailable(ApplicableTo applicableTo)
{
// Check if any applicability has unavailable lifecycle
if (applicableTo.Stack is not null &&
applicableTo.Stack.Any(a => a.Lifecycle == ProductLifecycle.Unavailable))
return true;

if (applicableTo.Serverless is not null)
{
var serverless = applicableTo.Serverless;
if ((serverless.Elasticsearch?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
(serverless.Observability?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
(serverless.Security?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false))
return true;
}

if (applicableTo.Deployment is not null)
{
var deployment = applicableTo.Deployment;
if ((deployment.Ess?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
(deployment.Ece?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
(deployment.Eck?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
(deployment.Self?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false))
return true;
}

return false;
}

private static ApplicabilityCategory GetPrimaryCategory(ApplicableTo applicableTo)
{
// Serverless takes priority
if (applicableTo.Serverless is not null)
return ApplicabilityCategory.Serverless;

// Then Stack
if (applicableTo.Stack is not null)
return ApplicabilityCategory.Stack;

// Then Deployment
if (applicableTo.Deployment is not null)
return ApplicabilityCategory.Deployment;

// Default
return ApplicabilityCategory.Other;
}

private static int CompareStack(ApplicableTo x, ApplicableTo y)
{
// Stack: order from latest to oldest version
var xVersion = GetLatestVersion(x.Stack);
var yVersion = GetLatestVersion(y.Stack);

if (xVersion is null && yVersion is null)
return 0;
if (xVersion is null)
return 1;
if (yVersion is null)
return -1;

// Higher version comes first (descending order)
return yVersion.CompareTo(xVersion);
}

private static int CompareDeployment(ApplicableTo x, ApplicableTo y)
{
// Deployment order: ech/ess, ece, eck, self-managed
var xDeploymentType = GetPrimaryDeploymentType(x.Deployment!);
var yDeploymentType = GetPrimaryDeploymentType(y.Deployment!);

return xDeploymentType.CompareTo(yDeploymentType);
}

private static DeploymentType GetPrimaryDeploymentType(DeploymentApplicability deployment)
{
// Return the first deployment type found in priority order
if (deployment.Ess is not null)
return DeploymentType.Ech; // ESS = ECH (Elastic Cloud Hosted)
if (deployment.Ece is not null)
return DeploymentType.Ece;
if (deployment.Eck is not null)
return DeploymentType.Eck;
if (deployment.Self is not null)
return DeploymentType.SelfManaged;

return DeploymentType.Unknown;
}

private static SemVersion? GetLatestVersion(AppliesCollection? collection)
{
if (collection is null || collection.Count == 0)
return null;

// Find the highest version in the collection
SemVersion? latest = null;
foreach (var applicability in collection)
{
var version = applicability.Version?.Min;
if (version is not null && (latest is null || version > latest))
latest = version;
}

return latest;
}

private enum ApplicabilityCategory
{
Serverless = 0, // Serverless first
Stack = 1, // Stack second
Deployment = 2, // Deployment third
Other = 3 // Everything else last
}

private enum DeploymentType
{
Ech = 0, // ECH/ESS first
Ece = 1, // ECE second
Eck = 2, // ECK third
SelfManaged = 3, // Self-managed last
Unknown = 4
}
}
15 changes: 10 additions & 5 deletions src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ public class ApplicableToViewModel

private static readonly Dictionary<Func<DeploymentApplicability, AppliesCollection?>, ApplicabilityMappings.ApplicabilityDefinition> DeploymentMappings = new()
{
[d => d.Ess] = ApplicabilityMappings.Ech,
[d => d.Eck] = ApplicabilityMappings.Eck,
[d => d.Ece] = ApplicabilityMappings.Ece,
[d => d.Self] = ApplicabilityMappings.Self
[d => d.Ess] = ApplicabilityMappings.Ech, // ESS/ECH first
[d => d.Ece] = ApplicabilityMappings.Ece, // ECE second
[d => d.Eck] = ApplicabilityMappings.Eck, // ECK third
[d => d.Self] = ApplicabilityMappings.Self // Self-managed last
};

private static readonly Dictionary<Func<ServerlessProjectApplicability, AppliesCollection?>, ApplicabilityMappings.ApplicabilityDefinition> ServerlessMappings = new()
Expand Down Expand Up @@ -81,7 +81,12 @@ public IReadOnlyCollection<ApplicabilityItem> GetApplicabilityItems()
if (AppliesTo.Product is not null)
rawItems.AddRange(CollectFromCollection(AppliesTo.Product, ApplicabilityMappings.Product));

return RenderGroupedItems(rawItems).ToArray();
var items = RenderGroupedItems(rawItems).ToList();

// Sort badges: unavailable lifecycle comes last
return items
.OrderBy(item => item.PrimaryApplicability.Lifecycle == ProductLifecycle.Unavailable ? 1 : 0)
.ToArray();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ public class AppliesSwitchBlock(DirectiveBlockParser parser, ParserContext conte
public int Index { get; set; }
public string GetGroupKey() => Prop("group") ?? "applies-switches";

public override void FinalizeAndValidate(ParserContext context) => Index = FindIndex();
public override void FinalizeAndValidate(ParserContext context)
{
Index = FindIndex();
SortAppliesItems();
}

private int _index = -1;

Expand All @@ -30,6 +34,52 @@ public int FindIndex()
_index = GetUniqueLineIndex();
return _index;
}

private void SortAppliesItems()
{
// Get all applies-item children
var items = this.OfType<AppliesItemBlock>().ToList();
if (items.Count <= 1)
return; // No need to sort if 0 or 1 items

// Parse ApplicableTo for each item for sorting
var itemsWithAppliesTo = items.Select(item =>
{
try
{
var applicableTo = YamlSerialization.Deserialize<ApplicableTo>(
item.AppliesToDefinition,
Build.ProductsConfiguration);
return (Item: item, AppliesTo: (ApplicableTo?)applicableTo);
}
catch
{
// If parsing fails, keep original order for this item
return (Item: item, AppliesTo: null);
}
Comment on lines +55 to +59
}).ToList();

// Create comparer
var comparer = new ApplicableToOrderComparer();

// Sort items based on their ApplicableTo, putting unparseable items at the end
var sorted = itemsWithAppliesTo
.OrderBy(x => x.AppliesTo is null ? 1 : 0) // Unparseable items last
.ThenBy(x => x.AppliesTo, comparer)
.ToList();

// Remove all items from the block
foreach (var item in items)
_ = Remove(item);

// Re-add items in sorted order
foreach (var (item, _) in sorted)
Add(item);

// Update indices after sorting
foreach (var item in items)
item.UpdateIndex();
}
}

public class AppliesItemBlock(DirectiveBlockParser parser, ParserContext context)
Expand All @@ -51,7 +101,6 @@ public override void FinalizeAndValidate(ParserContext context)
this.EmitError("{applies-item} requires an argument with applies_to definition.");

AppliesToDefinition = (Arguments ?? "{undefined}").ReplaceSubstitutions(context);
Index = Parent!.IndexOf(this);

var appliesSwitch = Parent as AppliesSwitchBlock;

Expand All @@ -63,6 +112,9 @@ public override void FinalizeAndValidate(ParserContext context)
Selected = PropBool("selected");
}

// Called after sorting to update the index
internal void UpdateIndex() => Index = Parent!.IndexOf(this);

public static string GenerateSyncKey(string appliesToDefinition, ProductsConfiguration productsConfiguration)
{
var applicableTo = YamlSerialization.Deserialize<ApplicableTo>(appliesToDefinition, productsConfiguration);
Expand Down