Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
39 changes: 39 additions & 0 deletions docs/syntax/applies-switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,45 @@ Content for Serverless
:::::
::::::

## Automatic ordering

Applies-switch tabs are **automatically ordered** according to documentation standards, regardless of how they appear in the source file. This ensures consistency across documentation.

The ordering rules are:

1. **Serverless** appears first
2. **Stack** versions appear second, ordered from **latest to oldest**
3. **Deployment types** appear third, in this order:
- ECH/ESS (Elastic Cloud Hosted)
- ECE (Elastic Cloud Enterprise)
- ECK (Elastic Cloud on Kubernetes)
- Self-managed
4. Items with **unavailable** lifecycle always appear **last**

**Example:**

Even if you write the tabs in a different order:

```markdown
::::{applies-switch}
:::{applies-item} stack: ga 8.11
Old stack version
:::
:::{applies-item} serverless: ga
Serverless content
:::
:::{applies-item} stack: preview 9.1
New stack version
:::
::::
```

They will automatically render in the correct order: Serverless → Stack 9.1 → Stack 8.11

:::{tip}
You don't need to manually order tabs anymore. Focus on content, and the system will handle the ordering for consistency.
:::

## Automatic grouping

All applies switches on a page automatically sync together. When you select an applies_to definition in one switch, all other switches will switch to the same applies_to definition.
Expand Down
18 changes: 12 additions & 6 deletions docs/syntax/applies.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,18 +369,24 @@ This section provides detailed rules for how badges are rendered based on lifecy

### Rendering order

`applies_to` badges are displayed in a consistent order regardless of how they appear in your source files:

1. **Stack** - Elastic Stack
2. **Serverless** - Elastic Cloud Serverless offerings
3. **Deployment** - Deployment options (ECH, ECK, ECE, Self-managed)
`applies_to` badges are **automatically displayed in a consistent order** regardless of how they appear in your source files:

1. **Serverless** - Elastic Cloud Serverless offerings (appears first)
2. **Stack** - Elastic Stack (ordered from latest to oldest version)
3. **Deployment** - Deployment options in this order:
- ECH/ESS (Elastic Cloud Hosted)
- ECE (Elastic Cloud Enterprise)
- ECK (Elastic Cloud on Kubernetes)
- Self-managed
4. **ProductApplicability** - Specialized tools and agents (ECCTL, Curator, EDOT, APM Agents)
5. **Product (generic)** - Generic product applicability

Items with **unavailable** lifecycle always appear **last**, regardless of their category.

Within the ProductApplicability category, EDOT and APM Agent items are sorted alphabetically for better scanning.

:::{note}
Inline applies annotations are rendered in the order they appear in the source file.
This automatic ordering applies to badges displayed at page and section levels, as well as in [applies-switch](applies-switch.md) tabs. Inline applies annotations are rendered in the order they appear in the source file.
:::

### Badge rendering rules
Expand Down
174 changes: 174 additions & 0 deletions src/Elastic.Documentation/AppliesTo/ApplicableToOrderComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// 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

using System.Linq;

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 using LINQ
return collection
.Select(applicability => applicability.Version?.Min)
.Where(version => version is not null)
.DefaultIfEmpty(null)
.Max();
}

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
Loading
Loading